From e690ae764baa2e97e78b2bed5a16d9f9357ecc19 Mon Sep 17 00:00:00 2001 From: Star Date: Fri, 20 Sep 2024 16:50:35 +0800 Subject: [PATCH] add http --- ai/ai.go | 99 +- ai/watcher/watcher.go | 183 + go.mod | 7 +- goja/LICENSE | 15 + goja/README.md | 334 ++ goja/array.go | 565 +++ goja/array_sparse.go | 500 ++ goja/array_sparse_test.go | 264 ++ goja/array_test.go | 133 + goja/ast/README.markdown | 1068 +++++ goja/ast/node.go | 876 ++++ goja/builtin_array.go | 1794 +++++++ goja/builtin_arrray_test.go | 367 ++ goja/builtin_bigint.go | 369 ++ goja/builtin_bigint_test.go | 117 + goja/builtin_boolean.go | 75 + goja/builtin_date.go | 1058 +++++ goja/builtin_error.go | 314 ++ goja/builtin_function.go | 416 ++ goja/builtin_function_test.go | 14 + goja/builtin_global.go | 576 +++ goja/builtin_global_test.go | 21 + goja/builtin_json.go | 542 +++ goja/builtin_json_test.go | 140 + goja/builtin_map.go | 342 ++ goja/builtin_map_test.go | 244 + goja/builtin_math.go | 358 ++ goja/builtin_number.go | 303 ++ goja/builtin_object.go | 711 +++ goja/builtin_promise.go | 646 +++ goja/builtin_proxy.go | 396 ++ goja/builtin_proxy_test.go | 1275 +++++ goja/builtin_reflect.go | 140 + goja/builtin_regexp.go | 1348 ++++++ goja/builtin_set.go | 346 ++ goja/builtin_set_test.go | 180 + goja/builtin_string.go | 1112 +++++ goja/builtin_string_test.go | 288 ++ goja/builtin_symbol.go | 177 + goja/builtin_typedarrays.go | 1973 ++++++++ goja/builtin_typedarrays_test.go | 326 ++ goja/builtin_weakmap.go | 176 + goja/builtin_weakmap_test.go | 76 + goja/builtin_weakset.go | 135 + goja/builtin_weakset_test.go | 101 + goja/compiler.go | 1481 ++++++ goja/compiler_expr.go | 3613 +++++++++++++++ goja/compiler_stmt.go | 1127 +++++ goja/compiler_test.go | 5900 +++++++++++++++++++++++ goja/date.go | 170 + goja/date_parser.go | 869 ++++ goja/date_parser_test.go | 31 + goja/date_test.go | 309 ++ goja/destruct.go | 300 ++ goja/extract_failed_tests.sh | 3 + goja/file/README.markdown | 110 + goja/file/file.go | 234 + goja/file/file_test.go | 75 + goja/ftoa/LICENSE_LUCENE | 21 + goja/ftoa/common.go | 150 + goja/ftoa/ftoa.go | 699 +++ goja/ftoa/ftobasestr.go | 153 + goja/ftoa/ftobasestr_test.go | 9 + goja/ftoa/ftostr.go | 147 + goja/ftoa/ftostr_test.go | 92 + goja/ftoa/internal/fast/LICENSE_V8 | 26 + goja/ftoa/internal/fast/cachedpower.go | 120 + goja/ftoa/internal/fast/common.go | 18 + goja/ftoa/internal/fast/diyfp.go | 152 + goja/ftoa/internal/fast/dtoa.go | 642 +++ goja/func.go | 1116 +++++ goja/func_test.go | 309 ++ goja/go.mod | 15 + goja/goja/main.go | 127 + goja/ipow.go | 98 + goja/map.go | 169 + goja/map_test.go | 201 + goja/object.go | 1824 ++++++++ goja/object_args.go | 139 + goja/object_dynamic.go | 794 ++++ goja/object_dynamic_test.go | 420 ++ goja/object_goarray_reflect.go | 358 ++ goja/object_goarray_reflect_test.go | 304 ++ goja/object_gomap.go | 158 + goja/object_gomap_reflect.go | 294 ++ goja/object_gomap_reflect_test.go | 350 ++ goja/object_gomap_test.go | 328 ++ goja/object_goreflect.go | 674 +++ goja/object_goreflect_test.go | 1667 +++++++ goja/object_goslice.go | 343 ++ goja/object_goslice_reflect.go | 89 + goja/object_goslice_reflect_test.go | 520 +++ goja/object_goslice_test.go | 298 ++ goja/object_template.go | 469 ++ goja/object_test.go | 668 +++ goja/parser/README.markdown | 184 + goja/parser/error.go | 176 + goja/parser/expression.go | 1660 +++++++ goja/parser/lexer.go | 1205 +++++ goja/parser/lexer_test.go | 423 ++ goja/parser/marshal_test.go | 890 ++++ goja/parser/parser.go | 268 ++ goja/parser/parser_test.go | 1334 ++++++ goja/parser/regexp.go | 472 ++ goja/parser/regexp_test.go | 191 + goja/parser/scope.go | 50 + goja/parser/statement.go | 1078 +++++ goja/parser/testutil_test.go | 49 + goja/profiler.go | 350 ++ goja/profiler_test.go | 102 + goja/proxy.go | 1074 +++++ goja/regexp.go | 650 +++ goja/regexp_test.go | 960 ++++ goja/runtime.go | 3194 +++++++++++++ goja/runtime_test.go | 3140 +++++++++++++ goja/staticcheck.conf | 1 + goja/string.go | 364 ++ goja/string_ascii.go | 401 ++ goja/string_imported.go | 307 ++ goja/string_test.go | 194 + goja/string_unicode.go | 619 +++ goja/tc39_norace_test.go | 19 + goja/tc39_race_test.go | 32 + goja/tc39_test.go | 755 +++ goja/testdata/S15.10.2.12_A1_T1.js | 529 +++ goja/token/Makefile | 2 + goja/token/README.markdown | 171 + goja/token/token.go | 122 + goja/token/token_const.go | 400 ++ goja/token/tokenfmt | 222 + goja/typedarrays.go | 1327 ++++++ goja/typedarrays_test.go | 544 +++ goja/unistring/string.go | 137 + goja/unistring/string_test.go | 16 + goja/value.go | 1196 +++++ goja/vm.go | 5917 ++++++++++++++++++++++++ goja/vm_test.go | 285 ++ js.go | 97 +- js/ai.go | 40 +- js/common.go | 84 + js/file.go | 10 +- js/http.go | 151 + js/lib/http.ts | 45 + js/lib/util.ts | 4 +- js/util.go | 59 +- tests/chat_test.go | 8 +- 146 files changed, 80103 insertions(+), 88 deletions(-) create mode 100644 ai/watcher/watcher.go create mode 100644 goja/LICENSE create mode 100644 goja/README.md create mode 100644 goja/array.go create mode 100644 goja/array_sparse.go create mode 100644 goja/array_sparse_test.go create mode 100644 goja/array_test.go create mode 100644 goja/ast/README.markdown create mode 100644 goja/ast/node.go create mode 100644 goja/builtin_array.go create mode 100644 goja/builtin_arrray_test.go create mode 100644 goja/builtin_bigint.go create mode 100644 goja/builtin_bigint_test.go create mode 100644 goja/builtin_boolean.go create mode 100644 goja/builtin_date.go create mode 100644 goja/builtin_error.go create mode 100644 goja/builtin_function.go create mode 100644 goja/builtin_function_test.go create mode 100644 goja/builtin_global.go create mode 100644 goja/builtin_global_test.go create mode 100644 goja/builtin_json.go create mode 100644 goja/builtin_json_test.go create mode 100644 goja/builtin_map.go create mode 100644 goja/builtin_map_test.go create mode 100644 goja/builtin_math.go create mode 100644 goja/builtin_number.go create mode 100644 goja/builtin_object.go create mode 100644 goja/builtin_promise.go create mode 100644 goja/builtin_proxy.go create mode 100644 goja/builtin_proxy_test.go create mode 100644 goja/builtin_reflect.go create mode 100644 goja/builtin_regexp.go create mode 100644 goja/builtin_set.go create mode 100644 goja/builtin_set_test.go create mode 100644 goja/builtin_string.go create mode 100644 goja/builtin_string_test.go create mode 100644 goja/builtin_symbol.go create mode 100644 goja/builtin_typedarrays.go create mode 100644 goja/builtin_typedarrays_test.go create mode 100644 goja/builtin_weakmap.go create mode 100644 goja/builtin_weakmap_test.go create mode 100644 goja/builtin_weakset.go create mode 100644 goja/builtin_weakset_test.go create mode 100644 goja/compiler.go create mode 100644 goja/compiler_expr.go create mode 100644 goja/compiler_stmt.go create mode 100644 goja/compiler_test.go create mode 100644 goja/date.go create mode 100644 goja/date_parser.go create mode 100644 goja/date_parser_test.go create mode 100644 goja/date_test.go create mode 100644 goja/destruct.go create mode 100755 goja/extract_failed_tests.sh create mode 100644 goja/file/README.markdown create mode 100644 goja/file/file.go create mode 100644 goja/file/file_test.go create mode 100644 goja/ftoa/LICENSE_LUCENE create mode 100644 goja/ftoa/common.go create mode 100644 goja/ftoa/ftoa.go create mode 100644 goja/ftoa/ftobasestr.go create mode 100644 goja/ftoa/ftobasestr_test.go create mode 100644 goja/ftoa/ftostr.go create mode 100644 goja/ftoa/ftostr_test.go create mode 100644 goja/ftoa/internal/fast/LICENSE_V8 create mode 100644 goja/ftoa/internal/fast/cachedpower.go create mode 100644 goja/ftoa/internal/fast/common.go create mode 100644 goja/ftoa/internal/fast/diyfp.go create mode 100644 goja/ftoa/internal/fast/dtoa.go create mode 100644 goja/func.go create mode 100644 goja/func_test.go create mode 100644 goja/go.mod create mode 100644 goja/goja/main.go create mode 100644 goja/ipow.go create mode 100644 goja/map.go create mode 100644 goja/map_test.go create mode 100644 goja/object.go create mode 100644 goja/object_args.go create mode 100644 goja/object_dynamic.go create mode 100644 goja/object_dynamic_test.go create mode 100644 goja/object_goarray_reflect.go create mode 100644 goja/object_goarray_reflect_test.go create mode 100644 goja/object_gomap.go create mode 100644 goja/object_gomap_reflect.go create mode 100644 goja/object_gomap_reflect_test.go create mode 100644 goja/object_gomap_test.go create mode 100644 goja/object_goreflect.go create mode 100644 goja/object_goreflect_test.go create mode 100644 goja/object_goslice.go create mode 100644 goja/object_goslice_reflect.go create mode 100644 goja/object_goslice_reflect_test.go create mode 100644 goja/object_goslice_test.go create mode 100644 goja/object_template.go create mode 100644 goja/object_test.go create mode 100644 goja/parser/README.markdown create mode 100644 goja/parser/error.go create mode 100644 goja/parser/expression.go create mode 100644 goja/parser/lexer.go create mode 100644 goja/parser/lexer_test.go create mode 100644 goja/parser/marshal_test.go create mode 100644 goja/parser/parser.go create mode 100644 goja/parser/parser_test.go create mode 100644 goja/parser/regexp.go create mode 100644 goja/parser/regexp_test.go create mode 100644 goja/parser/scope.go create mode 100644 goja/parser/statement.go create mode 100644 goja/parser/testutil_test.go create mode 100644 goja/profiler.go create mode 100644 goja/profiler_test.go create mode 100644 goja/proxy.go create mode 100644 goja/regexp.go create mode 100644 goja/regexp_test.go create mode 100644 goja/runtime.go create mode 100644 goja/runtime_test.go create mode 100644 goja/staticcheck.conf create mode 100644 goja/string.go create mode 100644 goja/string_ascii.go create mode 100644 goja/string_imported.go create mode 100644 goja/string_test.go create mode 100644 goja/string_unicode.go create mode 100644 goja/tc39_norace_test.go create mode 100644 goja/tc39_race_test.go create mode 100644 goja/tc39_test.go create mode 100644 goja/testdata/S15.10.2.12_A1_T1.js create mode 100644 goja/token/Makefile create mode 100644 goja/token/README.markdown create mode 100644 goja/token/token.go create mode 100644 goja/token/token_const.go create mode 100755 goja/token/tokenfmt create mode 100644 goja/typedarrays.go create mode 100644 goja/typedarrays_test.go create mode 100644 goja/unistring/string.go create mode 100644 goja/unistring/string_test.go create mode 100644 goja/value.go create mode 100644 goja/vm.go create mode 100644 goja/vm_test.go create mode 100644 js/common.go create mode 100644 js/http.go create mode 100644 js/lib/http.ts diff --git a/ai/ai.go b/ai/ai.go index 8eabdb8..7106c1f 100644 --- a/ai/ai.go +++ b/ai/ai.go @@ -2,17 +2,22 @@ package main import ( "apigo.cc/ai/ai" + "apigo.cc/ai/ai/ai/watcher" "fmt" "github.com/ssgo/u" "os" + "os/signal" + "path/filepath" "strings" + "syscall" + "time" ) func main() { if len(os.Args) > 1 && (os.Args[1] == "-e" || os.Args[1] == "export") { imports, err := ai.ExportForDev() if err != nil { - fmt.Println(err.Error()) + fmt.Println(u.BRed(err.Error())) } else { fmt.Println(`exported to ./lib example: @@ -28,32 +33,100 @@ function main(...args) { } return } else { + isWatch := false + var osArgs []string + if len(os.Args) > 1 && (os.Args[1] == "-w" || os.Args[1] == "watch") { + isWatch = true + osArgs = os.Args[2:] + } else { + osArgs = os.Args[1:] + } jsFile := "ai.js" - if len(os.Args) > 1 { - jsFile = os.Args[1] + if len(osArgs) > 0 { + jsFile = osArgs[0] if !strings.HasSuffix(jsFile, ".js") && !u.FileExists(jsFile) { jsFile = jsFile + ".js" } } + if u.FileExists(jsFile) { - args := make([]any, len(os.Args)-2) - for i := 2; i < len(os.Args); i++ { - args[i-2] = os.Args[i] + args := make([]any, len(osArgs)-1) + for i := 1; i < len(osArgs); i++ { + args[i-1] = osArgs[i] } - result, err := ai.RunFile(jsFile, args...) - if err != nil { - fmt.Println(err.Error()) - } else if result != nil { - fmt.Println(u.JsonP(result)) + var w *watcher.Watcher + run := func() { + rt := ai.New() + if w != nil { + rt.SetModuleLoader(func(filename string) string { + filePath := filepath.Dir(filename) + needWatch := true + for _, v := range w.WatchList() { + if v == filePath { + needWatch = false + break + } + } + if needWatch { + fmt.Println(u.BMagenta("[watching module path]"), filePath) + _ = w.Add(filePath) + } + return u.ReadFileN(filename) + }) + } + _, err := rt.StartFromFile(jsFile) + + result, err := rt.RunMain(args...) + if err != nil { + fmt.Println(u.BRed(err.Error())) + } else if result != nil { + fmt.Println(u.Cyan(u.JsonP(result))) + } + } + _, _ = os.Stdout.WriteString("\x1b[3;J\x1b[H\x1b[2J") + if isWatch { + watchStartPath := filepath.Dir(jsFile) + fmt.Println(u.BMagenta("[watching root path]"), watchStartPath) + var err error + var isWaitingRun = false + if w, err = watcher.Start([]string{watchStartPath}, []string{"js", "json", "yml"}, func(filename string, event string) { + _, _ = os.Stdout.WriteString("\x1b[3;J\x1b[H\x1b[2J") + fmt.Println(u.BYellow("[changed]"), filename) + if !isWaitingRun { + isWaitingRun = true + go func() { + time.Sleep(time.Millisecond * 50) + isWaitingRun = false + run() + }() + } + }); err == nil { + exitCh := make(chan os.Signal, 1) + closeCh := make(chan bool, 1) + signal.Notify(exitCh, os.Interrupt, syscall.SIGTERM, syscall.SIGINT, syscall.SIGHUP) + go func() { + <-exitCh + closeCh <- true + }() + run() + <-closeCh + w.Stop() + } else { + fmt.Println(u.BRed(err.Error())) + run() + } + } else { + run() } return } } fmt.Println(`Usage: -ai -h | --help show usage -ai -e | --export export ai.ts file for develop +ai -h | help show usage +ai -e | export export ai.ts file for develop ai test | test.js run test.js, if not specified, run ai.js +ai -w | watch test run test.js, if .js files changed will be reloaded `) return } diff --git a/ai/watcher/watcher.go b/ai/watcher/watcher.go new file mode 100644 index 0000000..370bece --- /dev/null +++ b/ai/watcher/watcher.go @@ -0,0 +1,183 @@ +package watcher + +import ( + "github.com/fsnotify/fsnotify" + "github.com/ssgo/u" + "os" + "path/filepath" + "strings" +) + +const ( + Create = "create" + Change = "change" + Remove = "remove" + Rename = "rename" +) + +type Watcher struct { + watcher *fsnotify.Watcher + isRunning bool + fileTypes []string + callback func(string, string) + stopChan chan bool +} + +func (w *Watcher) Stop() { + if !w.isRunning { + return + } + w.stopChan = make(chan bool, 1) + w.isRunning = false + if w.watcher != nil { + _ = w.watcher.Close() + } + <-w.stopChan + w.watcher = nil +} + +func (w *Watcher) inType(filename string) bool { + if len(w.fileTypes) == 0 { + return true + } + for _, fileType := range w.fileTypes { + if strings.HasSuffix(filename, fileType) { + return true + } + } + return false +} + +func (w *Watcher) Add(path string) error { + return w.add(path, false) +} + +func (w *Watcher) add(path string, checkFile bool) error { + if !w.isRunning { + return nil + } + if absPath, err := filepath.Abs(path); err == nil { + path = absPath + } + if !u.FileExists(path) { + _ = os.MkdirAll(path, 0755) + } + if err := w.watcher.Add(path); err != nil { + return err + } else { + var outErr error + for _, f := range u.ReadDirN(path) { + if !w.isRunning { + break + } + if f.IsDir { + if err := w.Add(f.FullName); err != nil { + outErr = err + } + } else if checkFile { + if w.inType(f.FullName) { + w.callback(f.FullName, Create) + } + } + } + return outErr + } +} + +func (w *Watcher) Remove(path string) { + if !w.isRunning { + return + } + eventFileDir := path + string(os.PathSeparator) + for _, item := range w.watcher.WatchList() { + if item == path || strings.HasPrefix(item, eventFileDir) { + _ = w.watcher.Remove(item) + } + } +} + +func (w *Watcher) SetFileTypes(fileTypes []string) { + if !w.isRunning { + return + } + if fileTypes == nil { + fileTypes = make([]string, 0) + } + for i, fileType := range fileTypes { + if !strings.HasPrefix(fileType, ".") { + fileTypes[i] = "." + fileType + } + } + w.fileTypes = fileTypes +} + +func (w *Watcher) WatchList() []string { + if !w.isRunning { + return nil + } + return w.watcher.WatchList() +} + +func Start(paths, fileTypes []string, callback func(filename string, event string)) (*Watcher, error) { + if watcher, err := fsnotify.NewWatcher(); err == nil { + if paths == nil { + paths = make([]string, 0) + } + w := &Watcher{ + watcher: watcher, + callback: callback, + isRunning: true, + } + w.SetFileTypes(fileTypes) + for _, path := range paths { + _ = w.add(path, false) + } + + go func() { + for w.isRunning { + select { + case event, ok := <-watcher.Events: + if !ok { + w.isRunning = false + break + } + + eventFilename := event.Name + if event.Has(fsnotify.Remove) || event.Has(fsnotify.Rename) { + w.Remove(eventFilename) + if w.inType(eventFilename) { + if event.Has(fsnotify.Remove) { + callback(eventFilename, Remove) + } else { + callback(eventFilename, Rename) + } + } + } else if event.Has(fsnotify.Write) { + if w.inType(eventFilename) { + callback(eventFilename, Change) + } + } else if event.Has(fsnotify.Create) { + fileInfo := u.GetFileInfo(event.Name) + if fileInfo.IsDir { + _ = w.add(eventFilename, true) + } else { + if w.inType(eventFilename) { + callback(eventFilename, Create) + } + } + } + case _, ok := <-w.watcher.Errors: + if !ok { + w.isRunning = false + break + } + } + } + w.stopChan <- true + }() + + return w, nil + } else { + return nil, err + } +} diff --git a/go.mod b/go.mod index 376e470..2311ad8 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,16 @@ go 1.22 require ( github.com/dop251/goja v0.0.0-20240828124009-016eb7256539 github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc + github.com/fsnotify/fsnotify v1.7.0 github.com/go-resty/resty/v2 v2.15.0 github.com/golang-jwt/jwt/v5 v5.2.1 github.com/sashabaranov/go-openai v1.29.2 github.com/ssgo/config v1.7.7 + github.com/ssgo/httpclient v1.7.7 github.com/ssgo/log v1.7.7 github.com/ssgo/u v1.7.7 github.com/stretchr/testify v1.9.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -22,6 +25,8 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/ssgo/standard v1.7.7 // indirect golang.org/x/net v0.29.0 // indirect + golang.org/x/sys v0.25.0 // indirect golang.org/x/text v0.18.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace github.com/dop251/goja v0.0.0-20240828124009-016eb7256539 => ./goja diff --git a/goja/LICENSE b/goja/LICENSE new file mode 100644 index 0000000..09c0004 --- /dev/null +++ b/goja/LICENSE @@ -0,0 +1,15 @@ +Copyright (c) 2016 Dmitry Panov + +Copyright (c) 2012 Robert Krimen + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/goja/README.md b/goja/README.md new file mode 100644 index 0000000..a4d0c09 --- /dev/null +++ b/goja/README.md @@ -0,0 +1,334 @@ +goja +==== + +ECMAScript 5.1(+) implementation in Go. + +[![Go Reference](https://pkg.go.dev/badge/github.com/dop251/goja.svg)](https://pkg.go.dev/github.com/dop251/goja) + +Goja is an implementation of ECMAScript 5.1 in pure Go with emphasis on standard compliance and +performance. + +This project was largely inspired by [otto](https://github.com/robertkrimen/otto). + +The minimum required Go version is 1.20. + +Features +-------- + + * Full ECMAScript 5.1 support (including regex and strict mode). + * Passes nearly all [tc39 tests](https://github.com/tc39/test262) for the features implemented so far. The goal is to + pass all of them. See .tc39_test262_checkout.sh for the latest working commit id. + * Capable of running Babel, Typescript compiler and pretty much anything written in ES5. + * Sourcemaps. + * Most of ES6 functionality, still work in progress, see https://github.com/dop251/goja/milestone/1?closed=1 + +Known incompatibilities and caveats +----------------------------------- + +### WeakMap +WeakMap is implemented by embedding references to the values into the keys. This means that as long +as the key is reachable all values associated with it in any weak maps also remain reachable and therefore +cannot be garbage collected even if they are not otherwise referenced, even after the WeakMap is gone. +The reference to the value is dropped either when the key is explicitly removed from the WeakMap or when the +key becomes unreachable. + +To illustrate this: + +```javascript +var m = new WeakMap(); +var key = {}; +var value = {/* a very large object */}; +m.set(key, value); +value = undefined; +m = undefined; // The value does NOT become garbage-collectable at this point +key = undefined; // Now it does +// m.delete(key); // This would work too +``` + +The reason for it is the limitation of the Go runtime. At the time of writing (version 1.15) having a finalizer +set on an object which is part of a reference cycle makes the whole cycle non-garbage-collectable. The solution +above is the only reasonable way I can think of without involving finalizers. This is the third attempt +(see https://github.com/dop251/goja/issues/250 and https://github.com/dop251/goja/issues/199 for more details). + +Note, this does not have any effect on the application logic, but may cause a higher-than-expected memory usage. + +### WeakRef and FinalizationRegistry +For the reason mentioned above implementing WeakRef and FinalizationRegistry does not seem to be possible at this stage. + +### JSON +`JSON.parse()` uses the standard Go library which operates in UTF-8. Therefore, it cannot correctly parse broken UTF-16 +surrogate pairs, for example: + +```javascript +JSON.parse(`"\\uD800"`).charCodeAt(0).toString(16) // returns "fffd" instead of "d800" +``` + +### Date +Conversion from calendar date to epoch timestamp uses the standard Go library which uses `int`, rather than `float` as per +ECMAScript specification. This means if you pass arguments that overflow int to the `Date()` constructor or if there is +an integer overflow, the result will be incorrect, for example: + +```javascript +Date.UTC(1970, 0, 1, 80063993375, 29, 1, -288230376151711740) // returns 29256 instead of 29312 +``` + +FAQ +--- + +### How fast is it? + +Although it's faster than many scripting language implementations in Go I have seen +(for example it's 6-7 times faster than otto on average) it is not a +replacement for V8 or SpiderMonkey or any other general-purpose JavaScript engine. +You can find some benchmarks [here](https://github.com/dop251/goja/issues/2). + +### Why would I want to use it over a V8 wrapper? + +It greatly depends on your usage scenario. If most of the work is done in javascript +(for example crypto or any other heavy calculations) you are definitely better off with V8. + +If you need a scripting language that drives an engine written in Go so that +you need to make frequent calls between Go and javascript passing complex data structures +then the cgo overhead may outweigh the benefits of having a faster javascript engine. + +Because it's written in pure Go there are no cgo dependencies, it's very easy to build and it +should run on any platform supported by Go. + +It gives you a much better control over execution environment so can be useful for research. + +### Is it goroutine-safe? + +No. An instance of goja.Runtime can only be used by a single goroutine +at a time. You can create as many instances of Runtime as you like but +it's not possible to pass object values between runtimes. + +### Where is setTimeout()? + +setTimeout() assumes concurrent execution of code which requires an execution +environment, for example an event loop similar to nodejs or a browser. +There is a [separate project](https://github.com/dop251/goja_nodejs) aimed at providing some NodeJS functionality, +and it includes an event loop. + +### Can you implement (feature X from ES6 or higher)? + +I will be adding features in their dependency order and as quickly as time permits. Please do not ask +for ETAs. Features that are open in the [milestone](https://github.com/dop251/goja/milestone/1) are either in progress +or will be worked on next. + +The ongoing work is done in separate feature branches which are merged into master when appropriate. +Every commit in these branches represents a relatively stable state (i.e. it compiles and passes all enabled tc39 tests), +however because the version of tc39 tests I use is quite old, it may be not as well tested as the ES5.1 functionality. Because there are (usually) no major breaking changes between ECMAScript revisions +it should not break your existing code. You are encouraged to give it a try and report any bugs found. Please do not submit fixes though without discussing it first, as the code could be changed in the meantime. + +### How do I contribute? + +Before submitting a pull request please make sure that: + +- You followed ECMA standard as close as possible. If adding a new feature make sure you've read the specification, +do not just base it on a couple of examples that work fine. +- Your change does not have a significant negative impact on performance (unless it's a bugfix and it's unavoidable) +- It passes all relevant tc39 tests. + +Current Status +-------------- + + * There should be no breaking changes in the API, however it may be extended. + * Some of the AnnexB functionality is missing. + +Basic Example +------------- + +Run JavaScript and get the result value. + +```go +vm := goja.New() +v, err := vm.RunString("2 + 2") +if err != nil { + panic(err) +} +if num := v.Export().(int64); num != 4 { + panic(num) +} +``` + +Passing Values to JS +-------------------- +Any Go value can be passed to JS using Runtime.ToValue() method. See the method's [documentation](https://pkg.go.dev/github.com/dop251/goja#Runtime.ToValue) for more details. + +Exporting Values from JS +------------------------ +A JS value can be exported into its default Go representation using Value.Export() method. + +Alternatively it can be exported into a specific Go variable using [Runtime.ExportTo()](https://pkg.go.dev/github.com/dop251/goja#Runtime.ExportTo) method. + +Within a single export operation the same Object will be represented by the same Go value (either the same map, slice or +a pointer to the same struct). This includes circular objects and makes it possible to export them. + +Calling JS functions from Go +---------------------------- +There are 2 approaches: + +- Using [AssertFunction()](https://pkg.go.dev/github.com/dop251/goja#AssertFunction): +```go +const SCRIPT = ` +function sum(a, b) { + return +a + b; +} +` + +vm := goja.New() +_, err := vm.RunString(SCRIPT) +if err != nil { + panic(err) +} +sum, ok := goja.AssertFunction(vm.Get("sum")) +if !ok { + panic("Not a function") +} + +res, err := sum(goja.Undefined(), vm.ToValue(40), vm.ToValue(2)) +if err != nil { + panic(err) +} +fmt.Println(res) +// Output: 42 +``` +- Using [Runtime.ExportTo()](https://pkg.go.dev/github.com/dop251/goja#Runtime.ExportTo): +```go +const SCRIPT = ` +function sum(a, b) { + return +a + b; +} +` + +vm := goja.New() +_, err := vm.RunString(SCRIPT) +if err != nil { + panic(err) +} + +var sum func(int, int) int +err = vm.ExportTo(vm.Get("sum"), &sum) +if err != nil { + panic(err) +} + +fmt.Println(sum(40, 2)) // note, _this_ value in the function will be undefined. +// Output: 42 +``` + +The first one is more low level and allows specifying _this_ value, whereas the second one makes the function look like +a normal Go function. + +Mapping struct field and method names +------------------------------------- +By default, the names are passed through as is which means they are capitalised. This does not match +the standard JavaScript naming convention, so if you need to make your JS code look more natural or if you are +dealing with a 3rd party library, you can use a [FieldNameMapper](https://pkg.go.dev/github.com/dop251/goja#FieldNameMapper): + +```go +vm := goja.New() +vm.SetFieldNameMapper(TagFieldNameMapper("json", true)) +type S struct { + Field int `json:"field"` +} +vm.Set("s", S{Field: 42}) +res, _ := vm.RunString(`s.field`) // without the mapper it would have been s.Field +fmt.Println(res.Export()) +// Output: 42 +``` + +There are two standard mappers: [TagFieldNameMapper](https://pkg.go.dev/github.com/dop251/goja#TagFieldNameMapper) and +[UncapFieldNameMapper](https://pkg.go.dev/github.com/dop251/goja#UncapFieldNameMapper), or you can use your own implementation. + +Native Constructors +------------------- + +In order to implement a constructor function in Go use `func (goja.ConstructorCall) *goja.Object`. +See [Runtime.ToValue()](https://pkg.go.dev/github.com/dop251/goja#Runtime.ToValue) documentation for more details. + +Regular Expressions +------------------- + +Goja uses the embedded Go regexp library where possible, otherwise it falls back to [regexp2](https://github.com/dlclark/regexp2). + +Exceptions +---------- + +Any exception thrown in JavaScript is returned as an error of type *Exception. It is possible to extract the value thrown +by using the Value() method: + +```go +vm := goja.New() +_, err := vm.RunString(` + +throw("Test"); + +`) + +if jserr, ok := err.(*Exception); ok { + if jserr.Value().Export() != "Test" { + panic("wrong value") + } +} else { + panic("wrong type") +} +``` + +If a native Go function panics with a Value, it is thrown as a Javascript exception (and therefore can be caught): + +```go +var vm *Runtime + +func Test() { + panic(vm.ToValue("Error")) +} + +vm = goja.New() +vm.Set("Test", Test) +_, err := vm.RunString(` + +try { + Test(); +} catch(e) { + if (e !== "Error") { + throw e; + } +} + +`) + +if err != nil { + panic(err) +} +``` + +Interrupting +------------ + +```go +func TestInterrupt(t *testing.T) { + const SCRIPT = ` + var i = 0; + for (;;) { + i++; + } + ` + + vm := goja.New() + time.AfterFunc(200 * time.Millisecond, func() { + vm.Interrupt("halt") + }) + + _, err := vm.RunString(SCRIPT) + if err == nil { + t.Fatal("Err is nil") + } + // err is of type *InterruptError and its Value() method returns whatever has been passed to vm.Interrupt() +} +``` + +NodeJS Compatibility +-------------------- + +There is a [separate project](https://github.com/dop251/goja_nodejs) aimed at providing some of the NodeJS functionality. diff --git a/goja/array.go b/goja/array.go new file mode 100644 index 0000000..7a67a47 --- /dev/null +++ b/goja/array.go @@ -0,0 +1,565 @@ +package goja + +import ( + "fmt" + "math" + "math/bits" + "reflect" + "strconv" + + "github.com/dop251/goja/unistring" +) + +type arrayIterObject struct { + baseObject + obj *Object + nextIdx int64 + kind iterationKind +} + +func (ai *arrayIterObject) next() Value { + if ai.obj == nil { + return ai.val.runtime.createIterResultObject(_undefined, true) + } + if ta, ok := ai.obj.self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + } + l := toLength(ai.obj.self.getStr("length", nil)) + index := ai.nextIdx + if index >= l { + ai.obj = nil + return ai.val.runtime.createIterResultObject(_undefined, true) + } + ai.nextIdx++ + idxVal := valueInt(index) + if ai.kind == iterationKindKey { + return ai.val.runtime.createIterResultObject(idxVal, false) + } + elementValue := nilSafe(ai.obj.self.getIdx(idxVal, nil)) + var result Value + if ai.kind == iterationKindValue { + result = elementValue + } else { + result = ai.val.runtime.newArrayValues([]Value{idxVal, elementValue}) + } + return ai.val.runtime.createIterResultObject(result, false) +} + +func (r *Runtime) createArrayIterator(iterObj *Object, kind iterationKind) Value { + o := &Object{runtime: r} + + ai := &arrayIterObject{ + obj: iterObj, + kind: kind, + } + ai.class = classObject + ai.val = o + ai.extensible = true + o.self = ai + ai.prototype = r.getArrayIteratorPrototype() + ai.init() + + return o +} + +type arrayObject struct { + baseObject + values []Value + length uint32 + objCount int + propValueCount int + lengthProp valueProperty +} + +func (a *arrayObject) init() { + a.baseObject.init() + a.lengthProp.writable = true + + a._put("length", &a.lengthProp) +} + +func (a *arrayObject) _setLengthInt(l uint32, throw bool) bool { + ret := true + if l <= a.length { + if a.propValueCount > 0 { + // Slow path + for i := len(a.values) - 1; i >= int(l); i-- { + if prop, ok := a.values[i].(*valueProperty); ok { + if !prop.configurable { + l = uint32(i) + 1 + ret = false + break + } + a.propValueCount-- + } + } + } + } + if l <= uint32(len(a.values)) { + if l >= 16 && l < uint32(cap(a.values))>>2 { + ar := make([]Value, l) + copy(ar, a.values) + a.values = ar + } else { + ar := a.values[l:len(a.values)] + for i := range ar { + ar[i] = nil + } + a.values = a.values[:l] + } + } + a.length = l + if !ret { + a.val.runtime.typeErrorResult(throw, "Cannot redefine property: length") + } + return ret +} + +func (a *arrayObject) setLengthInt(l uint32, throw bool) bool { + if l == a.length { + return true + } + if !a.lengthProp.writable { + a.val.runtime.typeErrorResult(throw, "length is not writable") + return false + } + return a._setLengthInt(l, throw) +} + +func (a *arrayObject) setLength(v uint32, throw bool) bool { + if !a.lengthProp.writable { + a.val.runtime.typeErrorResult(throw, "length is not writable") + return false + } + return a._setLengthInt(v, throw) +} + +func (a *arrayObject) getIdx(idx valueInt, receiver Value) Value { + prop := a.getOwnPropIdx(idx) + if prop == nil { + if a.prototype != nil { + if receiver == nil { + return a.prototype.self.getIdx(idx, a.val) + } + return a.prototype.self.getIdx(idx, receiver) + } + } + if prop, ok := prop.(*valueProperty); ok { + if receiver == nil { + return prop.get(a.val) + } + return prop.get(receiver) + } + return prop +} + +func (a *arrayObject) getOwnPropStr(name unistring.String) Value { + if len(a.values) > 0 { + if i := strToArrayIdx(name); i != math.MaxUint32 { + if i < uint32(len(a.values)) { + return a.values[i] + } + } + } + if name == "length" { + return a.getLengthProp() + } + return a.baseObject.getOwnPropStr(name) +} + +func (a *arrayObject) getOwnPropIdx(idx valueInt) Value { + if i := toIdx(idx); i != math.MaxUint32 { + if i < uint32(len(a.values)) { + return a.values[i] + } + return nil + } + + return a.baseObject.getOwnPropStr(idx.string()) +} + +func (a *arrayObject) sortLen() int { + return len(a.values) +} + +func (a *arrayObject) sortGet(i int) Value { + v := a.values[i] + if p, ok := v.(*valueProperty); ok { + v = p.get(a.val) + } + return v +} + +func (a *arrayObject) swap(i int, j int) { + a.values[i], a.values[j] = a.values[j], a.values[i] +} + +func (a *arrayObject) getStr(name unistring.String, receiver Value) Value { + return a.getStrWithOwnProp(a.getOwnPropStr(name), name, receiver) +} + +func (a *arrayObject) getLengthProp() *valueProperty { + a.lengthProp.value = intToValue(int64(a.length)) + return &a.lengthProp +} + +func (a *arrayObject) setOwnIdx(idx valueInt, val Value, throw bool) bool { + if i := toIdx(idx); i != math.MaxUint32 { + return a._setOwnIdx(i, val, throw) + } else { + return a.baseObject.setOwnStr(idx.string(), val, throw) + } +} + +func (a *arrayObject) _setOwnIdx(idx uint32, val Value, throw bool) bool { + var prop Value + if idx < uint32(len(a.values)) { + prop = a.values[idx] + } + + if prop == nil { + if proto := a.prototype; proto != nil { + // we know it's foreign because prototype loops are not allowed + if res, ok := proto.self.setForeignIdx(valueInt(idx), val, a.val, throw); ok { + return res + } + } + // new property + if !a.extensible { + a.val.runtime.typeErrorResult(throw, "Cannot add property %d, object is not extensible", idx) + return false + } else { + if idx >= a.length { + if !a.setLengthInt(idx+1, throw) { + return false + } + } + if idx >= uint32(len(a.values)) { + if !a.expand(idx) { + a.val.self.(*sparseArrayObject).add(idx, val) + return true + } + } + a.objCount++ + } + } else { + if prop, ok := prop.(*valueProperty); ok { + if !prop.isWritable() { + a.val.runtime.typeErrorResult(throw) + return false + } + prop.set(a.val, val) + return true + } + } + a.values[idx] = val + return true +} + +func (a *arrayObject) setOwnStr(name unistring.String, val Value, throw bool) bool { + if idx := strToArrayIdx(name); idx != math.MaxUint32 { + return a._setOwnIdx(idx, val, throw) + } else { + if name == "length" { + return a.setLength(a.val.runtime.toLengthUint32(val), throw) + } else { + return a.baseObject.setOwnStr(name, val, throw) + } + } +} + +func (a *arrayObject) setForeignIdx(idx valueInt, val, receiver Value, throw bool) (bool, bool) { + return a._setForeignIdx(idx, a.getOwnPropIdx(idx), val, receiver, throw) +} + +func (a *arrayObject) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { + return a._setForeignStr(name, a.getOwnPropStr(name), val, receiver, throw) +} + +type arrayPropIter struct { + a *arrayObject + limit int + idx int +} + +func (i *arrayPropIter) next() (propIterItem, iterNextFunc) { + for i.idx < len(i.a.values) && i.idx < i.limit { + name := asciiString(strconv.Itoa(i.idx)) + prop := i.a.values[i.idx] + i.idx++ + if prop != nil { + return propIterItem{name: name, value: prop}, i.next + } + } + + return i.a.baseObject.iterateStringKeys()() +} + +func (a *arrayObject) iterateStringKeys() iterNextFunc { + return (&arrayPropIter{ + a: a, + limit: len(a.values), + }).next +} + +func (a *arrayObject) stringKeys(all bool, accum []Value) []Value { + for i, prop := range a.values { + name := strconv.Itoa(i) + if prop != nil { + if !all { + if prop, ok := prop.(*valueProperty); ok && !prop.enumerable { + continue + } + } + accum = append(accum, asciiString(name)) + } + } + return a.baseObject.stringKeys(all, accum) +} + +func (a *arrayObject) hasOwnPropertyStr(name unistring.String) bool { + if idx := strToArrayIdx(name); idx != math.MaxUint32 { + return idx < uint32(len(a.values)) && a.values[idx] != nil + } else { + return a.baseObject.hasOwnPropertyStr(name) + } +} + +func (a *arrayObject) hasOwnPropertyIdx(idx valueInt) bool { + if idx := toIdx(idx); idx != math.MaxUint32 { + return idx < uint32(len(a.values)) && a.values[idx] != nil + } + return a.baseObject.hasOwnPropertyStr(idx.string()) +} + +func (a *arrayObject) hasPropertyIdx(idx valueInt) bool { + if a.hasOwnPropertyIdx(idx) { + return true + } + + if a.prototype != nil { + return a.prototype.self.hasPropertyIdx(idx) + } + + return false +} + +func (a *arrayObject) expand(idx uint32) bool { + targetLen := idx + 1 + if targetLen > uint32(len(a.values)) { + if targetLen < uint32(cap(a.values)) { + a.values = a.values[:targetLen] + } else { + if idx > 4096 && (a.objCount == 0 || idx/uint32(a.objCount) > 10) { + //log.Println("Switching standard->sparse") + sa := &sparseArrayObject{ + baseObject: a.baseObject, + length: a.length, + propValueCount: a.propValueCount, + } + sa.setValues(a.values, a.objCount+1) + sa.val.self = sa + sa.lengthProp.writable = a.lengthProp.writable + sa._put("length", &sa.lengthProp) + return false + } else { + if bits.UintSize == 32 { + if targetLen >= math.MaxInt32 { + panic(a.val.runtime.NewTypeError("Array index overflows int")) + } + } + tl := int(targetLen) + newValues := make([]Value, tl, growCap(tl, len(a.values), cap(a.values))) + copy(newValues, a.values) + a.values = newValues + } + } + } + return true +} + +func (r *Runtime) defineArrayLength(prop *valueProperty, descr PropertyDescriptor, setter func(uint32, bool) bool, throw bool) bool { + var newLen uint32 + ret := true + if descr.Value != nil { + newLen = r.toLengthUint32(descr.Value) + } + + if descr.Configurable == FLAG_TRUE || descr.Enumerable == FLAG_TRUE || descr.Getter != nil || descr.Setter != nil { + ret = false + goto Reject + } + + if descr.Value != nil { + oldLen := uint32(prop.value.ToInteger()) + if oldLen != newLen { + ret = setter(newLen, false) + } + } else { + ret = true + } + + if descr.Writable != FLAG_NOT_SET { + w := descr.Writable.Bool() + if prop.writable { + prop.writable = w + } else { + if w { + ret = false + goto Reject + } + } + } + +Reject: + if !ret { + r.typeErrorResult(throw, "Cannot redefine property: length") + } + + return ret +} + +func (a *arrayObject) _defineIdxProperty(idx uint32, desc PropertyDescriptor, throw bool) bool { + var existing Value + if idx < uint32(len(a.values)) { + existing = a.values[idx] + } + prop, ok := a.baseObject._defineOwnProperty(unistring.String(strconv.FormatUint(uint64(idx), 10)), existing, desc, throw) + if ok { + if idx >= a.length { + if !a.setLengthInt(idx+1, throw) { + return false + } + } + if a.expand(idx) { + a.values[idx] = prop + a.objCount++ + if _, ok := prop.(*valueProperty); ok { + a.propValueCount++ + } + } else { + a.val.self.(*sparseArrayObject).add(idx, prop) + } + } + return ok +} + +func (a *arrayObject) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + if idx := strToArrayIdx(name); idx != math.MaxUint32 { + return a._defineIdxProperty(idx, descr, throw) + } + if name == "length" { + return a.val.runtime.defineArrayLength(a.getLengthProp(), descr, a.setLength, throw) + } + return a.baseObject.defineOwnPropertyStr(name, descr, throw) +} + +func (a *arrayObject) defineOwnPropertyIdx(idx valueInt, descr PropertyDescriptor, throw bool) bool { + if idx := toIdx(idx); idx != math.MaxUint32 { + return a._defineIdxProperty(idx, descr, throw) + } + return a.baseObject.defineOwnPropertyStr(idx.string(), descr, throw) +} + +func (a *arrayObject) _deleteIdxProp(idx uint32, throw bool) bool { + if idx < uint32(len(a.values)) { + if v := a.values[idx]; v != nil { + if p, ok := v.(*valueProperty); ok { + if !p.configurable { + a.val.runtime.typeErrorResult(throw, "Cannot delete property '%d' of %s", idx, a.val.toString()) + return false + } + a.propValueCount-- + } + a.values[idx] = nil + a.objCount-- + } + } + return true +} + +func (a *arrayObject) deleteStr(name unistring.String, throw bool) bool { + if idx := strToArrayIdx(name); idx != math.MaxUint32 { + return a._deleteIdxProp(idx, throw) + } + return a.baseObject.deleteStr(name, throw) +} + +func (a *arrayObject) deleteIdx(idx valueInt, throw bool) bool { + if idx := toIdx(idx); idx != math.MaxUint32 { + return a._deleteIdxProp(idx, throw) + } + return a.baseObject.deleteStr(idx.string(), throw) +} + +func (a *arrayObject) export(ctx *objectExportCtx) interface{} { + if v, exists := ctx.get(a.val); exists { + return v + } + arr := make([]interface{}, a.length) + ctx.put(a.val, arr) + if a.propValueCount == 0 && a.length == uint32(len(a.values)) && uint32(a.objCount) == a.length { + for i, v := range a.values { + if v != nil { + arr[i] = exportValue(v, ctx) + } + } + } else { + for i := uint32(0); i < a.length; i++ { + v := a.getIdx(valueInt(i), nil) + if v != nil { + arr[i] = exportValue(v, ctx) + } + } + } + return arr +} + +func (a *arrayObject) exportType() reflect.Type { + return reflectTypeArray +} + +func (a *arrayObject) exportToArrayOrSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + r := a.val.runtime + if iter := a.getSym(SymIterator, nil); iter == r.getArrayValues() || iter == nil { + l := toIntStrict(int64(a.length)) + if typ.Kind() == reflect.Array { + if dst.Len() != l { + return fmt.Errorf("cannot convert an Array into an array, lengths mismatch (have %d, need %d)", l, dst.Len()) + } + } else { + dst.Set(reflect.MakeSlice(typ, l, l)) + } + ctx.putTyped(a.val, typ, dst.Interface()) + for i := 0; i < l; i++ { + if i >= len(a.values) { + break + } + val := a.values[i] + if p, ok := val.(*valueProperty); ok { + val = p.get(a.val) + } + err := r.toReflectValue(val, dst.Index(i), ctx) + if err != nil { + return fmt.Errorf("could not convert array element %v to %v at %d: %w", val, typ, i, err) + } + } + return nil + } + return a.baseObject.exportToArrayOrSlice(dst, typ, ctx) +} + +func (a *arrayObject) setValuesFromSparse(items []sparseArrayItem, newMaxIdx int) { + a.values = make([]Value, newMaxIdx+1) + for _, item := range items { + a.values[item.idx] = item.value + } + a.objCount = len(items) +} + +func toIdx(v valueInt) uint32 { + if v >= 0 && v < math.MaxUint32 { + return uint32(v) + } + return math.MaxUint32 +} diff --git a/goja/array_sparse.go b/goja/array_sparse.go new file mode 100644 index 0000000..f99afd7 --- /dev/null +++ b/goja/array_sparse.go @@ -0,0 +1,500 @@ +package goja + +import ( + "fmt" + "math" + "math/bits" + "reflect" + "sort" + "strconv" + + "github.com/dop251/goja/unistring" +) + +type sparseArrayItem struct { + idx uint32 + value Value +} + +type sparseArrayObject struct { + baseObject + items []sparseArrayItem + length uint32 + propValueCount int + lengthProp valueProperty +} + +func (a *sparseArrayObject) findIdx(idx uint32) int { + return sort.Search(len(a.items), func(i int) bool { + return a.items[i].idx >= idx + }) +} + +func (a *sparseArrayObject) _setLengthInt(l uint32, throw bool) bool { + ret := true + if l <= a.length { + if a.propValueCount > 0 { + // Slow path + for i := len(a.items) - 1; i >= 0; i-- { + item := a.items[i] + if item.idx <= l { + break + } + if prop, ok := item.value.(*valueProperty); ok { + if !prop.configurable { + l = item.idx + 1 + ret = false + break + } + a.propValueCount-- + } + } + } + } + + idx := a.findIdx(l) + + aa := a.items[idx:] + for i := range aa { + aa[i].value = nil + } + a.items = a.items[:idx] + a.length = l + if !ret { + a.val.runtime.typeErrorResult(throw, "Cannot redefine property: length") + } + return ret +} + +func (a *sparseArrayObject) setLengthInt(l uint32, throw bool) bool { + if l == a.length { + return true + } + if !a.lengthProp.writable { + a.val.runtime.typeErrorResult(throw, "length is not writable") + return false + } + return a._setLengthInt(l, throw) +} + +func (a *sparseArrayObject) setLength(v uint32, throw bool) bool { + if !a.lengthProp.writable { + a.val.runtime.typeErrorResult(throw, "length is not writable") + return false + } + return a._setLengthInt(v, throw) +} + +func (a *sparseArrayObject) _getIdx(idx uint32) Value { + i := a.findIdx(idx) + if i < len(a.items) && a.items[i].idx == idx { + return a.items[i].value + } + + return nil +} + +func (a *sparseArrayObject) getStr(name unistring.String, receiver Value) Value { + return a.getStrWithOwnProp(a.getOwnPropStr(name), name, receiver) +} + +func (a *sparseArrayObject) getIdx(idx valueInt, receiver Value) Value { + prop := a.getOwnPropIdx(idx) + if prop == nil { + if a.prototype != nil { + if receiver == nil { + return a.prototype.self.getIdx(idx, a.val) + } + return a.prototype.self.getIdx(idx, receiver) + } + } + if prop, ok := prop.(*valueProperty); ok { + if receiver == nil { + return prop.get(a.val) + } + return prop.get(receiver) + } + return prop +} + +func (a *sparseArrayObject) getLengthProp() *valueProperty { + a.lengthProp.value = intToValue(int64(a.length)) + return &a.lengthProp +} + +func (a *sparseArrayObject) getOwnPropStr(name unistring.String) Value { + if idx := strToArrayIdx(name); idx != math.MaxUint32 { + return a._getIdx(idx) + } + if name == "length" { + return a.getLengthProp() + } + return a.baseObject.getOwnPropStr(name) +} + +func (a *sparseArrayObject) getOwnPropIdx(idx valueInt) Value { + if idx := toIdx(idx); idx != math.MaxUint32 { + return a._getIdx(idx) + } + return a.baseObject.getOwnPropStr(idx.string()) +} + +func (a *sparseArrayObject) add(idx uint32, val Value) { + i := a.findIdx(idx) + a.items = append(a.items, sparseArrayItem{}) + copy(a.items[i+1:], a.items[i:]) + a.items[i] = sparseArrayItem{ + idx: idx, + value: val, + } +} + +func (a *sparseArrayObject) _setOwnIdx(idx uint32, val Value, throw bool) bool { + var prop Value + i := a.findIdx(idx) + if i < len(a.items) && a.items[i].idx == idx { + prop = a.items[i].value + } + + if prop == nil { + if proto := a.prototype; proto != nil { + // we know it's foreign because prototype loops are not allowed + if res, ok := proto.self.setForeignIdx(valueInt(idx), val, a.val, throw); ok { + return res + } + } + + // new property + if !a.extensible { + a.val.runtime.typeErrorResult(throw, "Cannot add property %d, object is not extensible", idx) + return false + } + + if idx >= a.length { + if !a.setLengthInt(idx+1, throw) { + return false + } + } + + if a.expand(idx) { + a.items = append(a.items, sparseArrayItem{}) + copy(a.items[i+1:], a.items[i:]) + a.items[i] = sparseArrayItem{ + idx: idx, + value: val, + } + } else { + ar := a.val.self.(*arrayObject) + ar.values[idx] = val + ar.objCount++ + return true + } + } else { + if prop, ok := prop.(*valueProperty); ok { + if !prop.isWritable() { + a.val.runtime.typeErrorResult(throw) + return false + } + prop.set(a.val, val) + } else { + a.items[i].value = val + } + } + return true +} + +func (a *sparseArrayObject) setOwnStr(name unistring.String, val Value, throw bool) bool { + if idx := strToArrayIdx(name); idx != math.MaxUint32 { + return a._setOwnIdx(idx, val, throw) + } else { + if name == "length" { + return a.setLength(a.val.runtime.toLengthUint32(val), throw) + } else { + return a.baseObject.setOwnStr(name, val, throw) + } + } +} + +func (a *sparseArrayObject) setOwnIdx(idx valueInt, val Value, throw bool) bool { + if idx := toIdx(idx); idx != math.MaxUint32 { + return a._setOwnIdx(idx, val, throw) + } + + return a.baseObject.setOwnStr(idx.string(), val, throw) +} + +func (a *sparseArrayObject) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { + return a._setForeignStr(name, a.getOwnPropStr(name), val, receiver, throw) +} + +func (a *sparseArrayObject) setForeignIdx(name valueInt, val, receiver Value, throw bool) (bool, bool) { + return a._setForeignIdx(name, a.getOwnPropIdx(name), val, receiver, throw) +} + +type sparseArrayPropIter struct { + a *sparseArrayObject + idx int +} + +func (i *sparseArrayPropIter) next() (propIterItem, iterNextFunc) { + for i.idx < len(i.a.items) { + name := asciiString(strconv.Itoa(int(i.a.items[i.idx].idx))) + prop := i.a.items[i.idx].value + i.idx++ + if prop != nil { + return propIterItem{name: name, value: prop}, i.next + } + } + + return i.a.baseObject.iterateStringKeys()() +} + +func (a *sparseArrayObject) iterateStringKeys() iterNextFunc { + return (&sparseArrayPropIter{ + a: a, + }).next +} + +func (a *sparseArrayObject) stringKeys(all bool, accum []Value) []Value { + if all { + for _, item := range a.items { + accum = append(accum, asciiString(strconv.FormatUint(uint64(item.idx), 10))) + } + } else { + for _, item := range a.items { + if prop, ok := item.value.(*valueProperty); ok && !prop.enumerable { + continue + } + accum = append(accum, asciiString(strconv.FormatUint(uint64(item.idx), 10))) + } + } + + return a.baseObject.stringKeys(all, accum) +} + +func (a *sparseArrayObject) setValues(values []Value, objCount int) { + a.items = make([]sparseArrayItem, 0, objCount) + for i, val := range values { + if val != nil { + a.items = append(a.items, sparseArrayItem{ + idx: uint32(i), + value: val, + }) + } + } +} + +func (a *sparseArrayObject) hasOwnPropertyStr(name unistring.String) bool { + if idx := strToArrayIdx(name); idx != math.MaxUint32 { + i := a.findIdx(idx) + return i < len(a.items) && a.items[i].idx == idx + } else { + return a.baseObject.hasOwnPropertyStr(name) + } +} + +func (a *sparseArrayObject) hasOwnPropertyIdx(idx valueInt) bool { + if idx := toIdx(idx); idx != math.MaxUint32 { + i := a.findIdx(idx) + return i < len(a.items) && a.items[i].idx == idx + } + + return a.baseObject.hasOwnPropertyStr(idx.string()) +} + +func (a *sparseArrayObject) hasPropertyIdx(idx valueInt) bool { + if a.hasOwnPropertyIdx(idx) { + return true + } + + if a.prototype != nil { + return a.prototype.self.hasPropertyIdx(idx) + } + + return false +} + +func (a *sparseArrayObject) expand(idx uint32) bool { + if l := len(a.items); l >= 1024 { + if ii := a.items[l-1].idx; ii > idx { + idx = ii + } + if (bits.UintSize == 64 || idx < math.MaxInt32) && int(idx)>>3 < l { + //log.Println("Switching sparse->standard") + ar := &arrayObject{ + baseObject: a.baseObject, + length: a.length, + propValueCount: a.propValueCount, + } + ar.setValuesFromSparse(a.items, int(idx)) + ar.val.self = ar + ar.lengthProp.writable = a.lengthProp.writable + a._put("length", &ar.lengthProp) + return false + } + } + return true +} + +func (a *sparseArrayObject) _defineIdxProperty(idx uint32, desc PropertyDescriptor, throw bool) bool { + var existing Value + i := a.findIdx(idx) + if i < len(a.items) && a.items[i].idx == idx { + existing = a.items[i].value + } + prop, ok := a.baseObject._defineOwnProperty(unistring.String(strconv.FormatUint(uint64(idx), 10)), existing, desc, throw) + if ok { + if idx >= a.length { + if !a.setLengthInt(idx+1, throw) { + return false + } + } + if i >= len(a.items) || a.items[i].idx != idx { + if a.expand(idx) { + a.items = append(a.items, sparseArrayItem{}) + copy(a.items[i+1:], a.items[i:]) + a.items[i] = sparseArrayItem{ + idx: idx, + value: prop, + } + if idx >= a.length { + a.length = idx + 1 + } + } else { + a.val.self.(*arrayObject).values[idx] = prop + } + } else { + a.items[i].value = prop + } + if _, ok := prop.(*valueProperty); ok { + a.propValueCount++ + } + } + return ok +} + +func (a *sparseArrayObject) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + if idx := strToArrayIdx(name); idx != math.MaxUint32 { + return a._defineIdxProperty(idx, descr, throw) + } + if name == "length" { + return a.val.runtime.defineArrayLength(a.getLengthProp(), descr, a.setLength, throw) + } + return a.baseObject.defineOwnPropertyStr(name, descr, throw) +} + +func (a *sparseArrayObject) defineOwnPropertyIdx(idx valueInt, descr PropertyDescriptor, throw bool) bool { + if idx := toIdx(idx); idx != math.MaxUint32 { + return a._defineIdxProperty(idx, descr, throw) + } + return a.baseObject.defineOwnPropertyStr(idx.string(), descr, throw) +} + +func (a *sparseArrayObject) _deleteIdxProp(idx uint32, throw bool) bool { + i := a.findIdx(idx) + if i < len(a.items) && a.items[i].idx == idx { + if p, ok := a.items[i].value.(*valueProperty); ok { + if !p.configurable { + a.val.runtime.typeErrorResult(throw, "Cannot delete property '%d' of %s", idx, a.val.toString()) + return false + } + a.propValueCount-- + } + copy(a.items[i:], a.items[i+1:]) + a.items[len(a.items)-1].value = nil + a.items = a.items[:len(a.items)-1] + } + return true +} + +func (a *sparseArrayObject) deleteStr(name unistring.String, throw bool) bool { + if idx := strToArrayIdx(name); idx != math.MaxUint32 { + return a._deleteIdxProp(idx, throw) + } + return a.baseObject.deleteStr(name, throw) +} + +func (a *sparseArrayObject) deleteIdx(idx valueInt, throw bool) bool { + if idx := toIdx(idx); idx != math.MaxUint32 { + return a._deleteIdxProp(idx, throw) + } + return a.baseObject.deleteStr(idx.string(), throw) +} + +func (a *sparseArrayObject) sortLen() int { + if len(a.items) > 0 { + return toIntStrict(int64(a.items[len(a.items)-1].idx) + 1) + } + + return 0 +} + +func (a *sparseArrayObject) export(ctx *objectExportCtx) interface{} { + if v, exists := ctx.get(a.val); exists { + return v + } + arr := make([]interface{}, a.length) + ctx.put(a.val, arr) + var prevIdx uint32 + for _, item := range a.items { + idx := item.idx + for i := prevIdx; i < idx; i++ { + if a.prototype != nil { + if v := a.prototype.self.getIdx(valueInt(i), nil); v != nil { + arr[i] = exportValue(v, ctx) + } + } + } + v := item.value + if v != nil { + if prop, ok := v.(*valueProperty); ok { + v = prop.get(a.val) + } + arr[idx] = exportValue(v, ctx) + } + prevIdx = idx + 1 + } + for i := prevIdx; i < a.length; i++ { + if a.prototype != nil { + if v := a.prototype.self.getIdx(valueInt(i), nil); v != nil { + arr[i] = exportValue(v, ctx) + } + } + } + return arr +} + +func (a *sparseArrayObject) exportType() reflect.Type { + return reflectTypeArray +} + +func (a *sparseArrayObject) exportToArrayOrSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + r := a.val.runtime + if iter := a.getSym(SymIterator, nil); iter == r.getArrayValues() || iter == nil { + l := toIntStrict(int64(a.length)) + if typ.Kind() == reflect.Array { + if dst.Len() != l { + return fmt.Errorf("cannot convert an Array into an array, lengths mismatch (have %d, need %d)", l, dst.Len()) + } + } else { + dst.Set(reflect.MakeSlice(typ, l, l)) + } + ctx.putTyped(a.val, typ, dst.Interface()) + for _, item := range a.items { + val := item.value + if p, ok := val.(*valueProperty); ok { + val = p.get(a.val) + } + idx := toIntStrict(int64(item.idx)) + if idx >= l { + break + } + err := r.toReflectValue(val, dst.Index(idx), ctx) + if err != nil { + return fmt.Errorf("could not convert array element %v to %v at %d: %w", item.value, typ, idx, err) + } + } + return nil + } + return a.baseObject.exportToArrayOrSlice(dst, typ, ctx) +} diff --git a/goja/array_sparse_test.go b/goja/array_sparse_test.go new file mode 100644 index 0000000..9a635cf --- /dev/null +++ b/goja/array_sparse_test.go @@ -0,0 +1,264 @@ +package goja + +import ( + "testing" +) + +func TestSparseArraySetLengthWithPropItems(t *testing.T) { + const SCRIPT = ` + var a = [1,2,3,4]; + a[100000] = 5; + var thrown = false; + + Object.defineProperty(a, "2", {value: 42, configurable: false, writable: false}); + try { + Object.defineProperty(a, "length", {value: 0, writable: false}); + } catch (e) { + thrown = e instanceof TypeError; + } + thrown && a.length === 3; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestSparseArraySwitch(t *testing.T) { + vm := New() + _, err := vm.RunString(` + var a = []; + a[20470] = 5; // switch to sparse`) + if err != nil { + t.Fatal(err) + } + a := vm.Get("a").(*Object) + if _, ok := a.self.(*sparseArrayObject); !ok { + t.Fatal("1: array is not sparse") + } + _, err = vm.RunString(` + var cutoffIdx = Math.round(20470 - 20470/8); + for (var i = a.length - 1; i >= cutoffIdx; i--) { + a[i] = i; + } + + // At this point it will have switched to a normal array + if (a.length != 20471) { + throw new Error("Invalid length: " + a.length); + } + + for (var i = 0; i < cutoffIdx; i++) { + if (a[i] !== undefined) { + throw new Error("Invalid value at " + i + ": " + a[i]); + } + } + + for (var i = cutoffIdx; i < a.length; i++) { + if (a[i] !== i) { + throw new Error("Invalid value at " + i + ": " + a[i]); + } + }`) + if err != nil { + t.Fatal(err) + } + if _, ok := a.self.(*arrayObject); !ok { + t.Fatal("2: array is not normal") + } + _, err = vm.RunString(` + // Now try to expand. Should stay a normal array + a[20471] = 20471; + if (a.length != 20472) { + throw new Error("Invalid length: " + a.length); + } + + for (var i = 0; i < cutoffIdx; i++) { + if (a[i] !== undefined) { + throw new Error("Invalid value at " + i + ": " + a[i]); + } + } + + for (var i = cutoffIdx; i < a.length; i++) { + if (a[i] !== i) { + throw new Error("Invalid value at " + i + ": " + a[i]); + } + }`) + if err != nil { + t.Fatal(err) + } + if _, ok := a.self.(*arrayObject); !ok { + t.Fatal("3: array is not normal") + } + _, err = vm.RunString(` + // Delete enough elements for it to become sparse again. + var cutoffIdx1 = Math.round(20472 - 20472/10); + for (var i = cutoffIdx; i < cutoffIdx1; i++) { + delete a[i]; + } + + // This should switch it back to sparse. + a[25590] = 25590; + if (a.length != 25591) { + throw new Error("Invalid length: " + a.length); + } + + for (var i = 0; i < cutoffIdx1; i++) { + if (a[i] !== undefined) { + throw new Error("Invalid value at " + i + ": " + a[i]); + } + } + + for (var i = cutoffIdx1; i < 20472; i++) { + if (a[i] !== i) { + throw new Error("Invalid value at " + i + ": " + a[i]); + } + } + + for (var i = 20472; i < 25590; i++) { + if (a[i] !== undefined) { + throw new Error("Invalid value at " + i + ": " + a[i]); + } + } + + if (a[25590] !== 25590) { + throw new Error("Invalid value at 25590: " + a[25590]); + } + `) + if err != nil { + t.Fatal(err) + } + if _, ok := a.self.(*sparseArrayObject); !ok { + t.Fatal("4: array is not sparse") + } +} + +func TestSparseArrayOwnKeys(t *testing.T) { + const SCRIPT = ` + var a1 = []; + a1[500000] = 1; + var seen = false; + var count = 0; + var keys = Object.keys(a1); + keys.length === 1 && keys[0] === "500000"; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestSparseArrayEnumerate(t *testing.T) { + const SCRIPT = ` + var a1 = []; + a1[500000] = 1; + var seen = false; + var count = 0; + for (var i in a1) { + if (i === "500000") { + if (seen) { + throw new Error("seen twice"); + } + seen = true; + } + count++; + } + seen && count === 1; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestArraySparseMaxLength(t *testing.T) { + const SCRIPT = ` + var a = []; + a[4294967294]=1; + a.length === 4294967295 && a[4294967294] === 1; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestArraySparseExportProps(t *testing.T) { + vm := New() + proto := vm.NewArray() + for _, idx := range []string{"0", "500", "9999", "10001", "20471"} { + err := proto.Set(idx, true) + if err != nil { + t.Fatal(err) + } + } + + arr := vm.NewArray() + err := arr.SetPrototype(proto) + if err != nil { + t.Fatal(err) + } + err = arr.DefineDataProperty("20470", vm.ToValue(true), FLAG_TRUE, FLAG_FALSE, FLAG_TRUE) + if err != nil { + t.Fatal(err) + } + err = arr.DefineDataProperty("10000", vm.ToValue(true), FLAG_TRUE, FLAG_FALSE, FLAG_TRUE) + if err != nil { + t.Fatal(err) + } + err = arr.Set("length", 20472) + if err != nil { + t.Fatal(err) + } + actual := arr.Export() + if actualArr, ok := actual.([]interface{}); ok { + if len(actualArr) == 20472 { + expectedIdx := map[int]struct{}{ + 0: {}, + 500: {}, + 9999: {}, + 10000: {}, + 10001: {}, + 20470: {}, + 20471: {}, + } + for i, v := range actualArr { + if _, exists := expectedIdx[i]; exists { + if v != true { + t.Fatalf("Expected true at %d, got %v", i, v) + } + } else { + if v != nil { + t.Fatalf("Expected nil at %d, got %v", i, v) + } + } + } + } else { + t.Fatalf("Expected len 20471, actual: %d", len(actualArr)) + } + } else { + t.Fatalf("Invalid export type: %T", actual) + } +} + +func TestSparseArrayExportToSlice(t *testing.T) { + vm := New() + arr := vm.NewArray() + err := arr.Set("20470", 120470) + if err != nil { + t.Fatal(err) + } + err = arr.DefineDataProperty("20471", vm.ToValue(220471), FLAG_TRUE, FLAG_FALSE, FLAG_TRUE) + if err != nil { + t.Fatal(err) + } + var exp []int + err = vm.ExportTo(arr, &exp) + if err != nil { + t.Fatal(err) + } + if len(exp) != 20472 { + t.Fatalf("len: %d", len(exp)) + } + if e := exp[20470]; e != 120470 { + t.Fatalf("20470: %d", e) + } + if e := exp[20471]; e != 220471 { + t.Fatalf("20471: %d", e) + } + for i := 0; i < 20470; i++ { + if exp[i] != 0 { + t.Fatalf("at %d: %d", i, exp[i]) + } + } +} diff --git a/goja/array_test.go b/goja/array_test.go new file mode 100644 index 0000000..fbbd4aa --- /dev/null +++ b/goja/array_test.go @@ -0,0 +1,133 @@ +package goja + +import ( + "reflect" + "testing" +) + +func TestArray1(t *testing.T) { + r := &Runtime{} + a := r.newArray(nil) + a.setOwnIdx(valueInt(0), asciiString("test"), true) + if l := a.getStr("length", nil).ToInteger(); l != 1 { + t.Fatalf("Unexpected length: %d", l) + } +} + +func TestArrayExportProps(t *testing.T) { + vm := New() + arr := vm.NewArray() + err := arr.DefineDataProperty("0", vm.ToValue(true), FLAG_TRUE, FLAG_FALSE, FLAG_TRUE) + if err != nil { + t.Fatal(err) + } + actual := arr.Export() + expected := []interface{}{true} + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("Expected: %#v, actual: %#v", expected, actual) + } +} + +func TestArrayCanonicalIndex(t *testing.T) { + const SCRIPT = ` + var a = []; + a["00"] = 1; + a["01"] = 2; + if (a[0] !== undefined) { + throw new Error("a[0]"); + } + ` + + testScript(SCRIPT, _undefined, t) +} + +func BenchmarkArrayGetStr(b *testing.B) { + b.StopTimer() + r := New() + v := &Object{runtime: r} + + a := &arrayObject{ + baseObject: baseObject{ + val: v, + extensible: true, + }, + } + v.self = a + + a.init() + + v.setOwn(valueInt(0), asciiString("test"), false) + b.StartTimer() + + for i := 0; i < b.N; i++ { + a.getStr("0", nil) + } + +} + +func BenchmarkArrayGet(b *testing.B) { + b.StopTimer() + r := New() + v := &Object{runtime: r} + + a := &arrayObject{ + baseObject: baseObject{ + val: v, + extensible: true, + }, + } + v.self = a + + a.init() + + var idx Value = valueInt(0) + + v.setOwn(idx, asciiString("test"), false) + + b.StartTimer() + + for i := 0; i < b.N; i++ { + v.get(idx, nil) + } + +} + +func BenchmarkArrayPut(b *testing.B) { + b.StopTimer() + r := New() + + v := &Object{runtime: r} + + a := &arrayObject{ + baseObject: baseObject{ + val: v, + extensible: true, + }, + } + + v.self = a + + a.init() + + var idx Value = valueInt(0) + var val Value = asciiString("test") + + b.StartTimer() + + for i := 0; i < b.N; i++ { + v.setOwn(idx, val, false) + } + +} + +func BenchmarkArraySetEmpty(b *testing.B) { + r := New() + _ = r.Get("Array").(*Object).Get("prototype").String() // materialise Array.prototype + a := r.NewArray(0, 0) + values := a.self.(*arrayObject).values + b.ResetTimer() + for i := 0; i < b.N; i++ { + values[0] = nil + a.self.setOwnIdx(0, valueTrue, true) + } +} diff --git a/goja/ast/README.markdown b/goja/ast/README.markdown new file mode 100644 index 0000000..aba088e --- /dev/null +++ b/goja/ast/README.markdown @@ -0,0 +1,1068 @@ +# ast +-- + import "github.com/dop251/goja/ast" + +Package ast declares types representing a JavaScript AST. + + +### Warning + +The parser and AST interfaces are still works-in-progress (particularly where +node types are concerned) and may change in the future. + +## Usage + +#### type ArrayLiteral + +```go +type ArrayLiteral struct { + LeftBracket file.Idx + RightBracket file.Idx + Value []Expression +} +``` + + +#### func (*ArrayLiteral) Idx0 + +```go +func (self *ArrayLiteral) Idx0() file.Idx +``` + +#### func (*ArrayLiteral) Idx1 + +```go +func (self *ArrayLiteral) Idx1() file.Idx +``` + +#### type AssignExpression + +```go +type AssignExpression struct { + Operator token.Token + Left Expression + Right Expression +} +``` + + +#### func (*AssignExpression) Idx0 + +```go +func (self *AssignExpression) Idx0() file.Idx +``` + +#### func (*AssignExpression) Idx1 + +```go +func (self *AssignExpression) Idx1() file.Idx +``` + +#### type BadExpression + +```go +type BadExpression struct { + From file.Idx + To file.Idx +} +``` + + +#### func (*BadExpression) Idx0 + +```go +func (self *BadExpression) Idx0() file.Idx +``` + +#### func (*BadExpression) Idx1 + +```go +func (self *BadExpression) Idx1() file.Idx +``` + +#### type BadStatement + +```go +type BadStatement struct { + From file.Idx + To file.Idx +} +``` + + +#### func (*BadStatement) Idx0 + +```go +func (self *BadStatement) Idx0() file.Idx +``` + +#### func (*BadStatement) Idx1 + +```go +func (self *BadStatement) Idx1() file.Idx +``` + +#### type BinaryExpression + +```go +type BinaryExpression struct { + Operator token.Token + Left Expression + Right Expression + Comparison bool +} +``` + + +#### func (*BinaryExpression) Idx0 + +```go +func (self *BinaryExpression) Idx0() file.Idx +``` + +#### func (*BinaryExpression) Idx1 + +```go +func (self *BinaryExpression) Idx1() file.Idx +``` + +#### type BlockStatement + +```go +type BlockStatement struct { + LeftBrace file.Idx + List []Statement + RightBrace file.Idx +} +``` + + +#### func (*BlockStatement) Idx0 + +```go +func (self *BlockStatement) Idx0() file.Idx +``` + +#### func (*BlockStatement) Idx1 + +```go +func (self *BlockStatement) Idx1() file.Idx +``` + +#### type BooleanLiteral + +```go +type BooleanLiteral struct { + Idx file.Idx + Literal string + Value bool +} +``` + + +#### func (*BooleanLiteral) Idx0 + +```go +func (self *BooleanLiteral) Idx0() file.Idx +``` + +#### func (*BooleanLiteral) Idx1 + +```go +func (self *BooleanLiteral) Idx1() file.Idx +``` + +#### type BracketExpression + +```go +type BracketExpression struct { + Left Expression + Member Expression + LeftBracket file.Idx + RightBracket file.Idx +} +``` + + +#### func (*BracketExpression) Idx0 + +```go +func (self *BracketExpression) Idx0() file.Idx +``` + +#### func (*BracketExpression) Idx1 + +```go +func (self *BracketExpression) Idx1() file.Idx +``` + +#### type BranchStatement + +```go +type BranchStatement struct { + Idx file.Idx + Token token.Token + Label *Identifier +} +``` + + +#### func (*BranchStatement) Idx0 + +```go +func (self *BranchStatement) Idx0() file.Idx +``` + +#### func (*BranchStatement) Idx1 + +```go +func (self *BranchStatement) Idx1() file.Idx +``` + +#### type CallExpression + +```go +type CallExpression struct { + Callee Expression + LeftParenthesis file.Idx + ArgumentList []Expression + RightParenthesis file.Idx +} +``` + + +#### func (*CallExpression) Idx0 + +```go +func (self *CallExpression) Idx0() file.Idx +``` + +#### func (*CallExpression) Idx1 + +```go +func (self *CallExpression) Idx1() file.Idx +``` + +#### type CaseStatement + +```go +type CaseStatement struct { + Case file.Idx + Test Expression + Consequent []Statement +} +``` + + +#### func (*CaseStatement) Idx0 + +```go +func (self *CaseStatement) Idx0() file.Idx +``` + +#### func (*CaseStatement) Idx1 + +```go +func (self *CaseStatement) Idx1() file.Idx +``` + +#### type CatchStatement + +```go +type CatchStatement struct { + Catch file.Idx + Parameter *Identifier + Body Statement +} +``` + + +#### func (*CatchStatement) Idx0 + +```go +func (self *CatchStatement) Idx0() file.Idx +``` + +#### func (*CatchStatement) Idx1 + +```go +func (self *CatchStatement) Idx1() file.Idx +``` + +#### type ConditionalExpression + +```go +type ConditionalExpression struct { + Test Expression + Consequent Expression + Alternate Expression +} +``` + + +#### func (*ConditionalExpression) Idx0 + +```go +func (self *ConditionalExpression) Idx0() file.Idx +``` + +#### func (*ConditionalExpression) Idx1 + +```go +func (self *ConditionalExpression) Idx1() file.Idx +``` + +#### type DebuggerStatement + +```go +type DebuggerStatement struct { + Debugger file.Idx +} +``` + + +#### func (*DebuggerStatement) Idx0 + +```go +func (self *DebuggerStatement) Idx0() file.Idx +``` + +#### func (*DebuggerStatement) Idx1 + +```go +func (self *DebuggerStatement) Idx1() file.Idx +``` + +#### type Declaration + +```go +type Declaration interface { + // contains filtered or unexported methods +} +``` + +All declaration nodes implement the Declaration interface. + +#### type DoWhileStatement + +```go +type DoWhileStatement struct { + Do file.Idx + Test Expression + Body Statement +} +``` + + +#### func (*DoWhileStatement) Idx0 + +```go +func (self *DoWhileStatement) Idx0() file.Idx +``` + +#### func (*DoWhileStatement) Idx1 + +```go +func (self *DoWhileStatement) Idx1() file.Idx +``` + +#### type DotExpression + +```go +type DotExpression struct { + Left Expression + Identifier Identifier +} +``` + + +#### func (*DotExpression) Idx0 + +```go +func (self *DotExpression) Idx0() file.Idx +``` + +#### func (*DotExpression) Idx1 + +```go +func (self *DotExpression) Idx1() file.Idx +``` + +#### type EmptyStatement + +```go +type EmptyStatement struct { + Semicolon file.Idx +} +``` + + +#### func (*EmptyStatement) Idx0 + +```go +func (self *EmptyStatement) Idx0() file.Idx +``` + +#### func (*EmptyStatement) Idx1 + +```go +func (self *EmptyStatement) Idx1() file.Idx +``` + +#### type Expression + +```go +type Expression interface { + Node + // contains filtered or unexported methods +} +``` + +All expression nodes implement the Expression interface. + +#### type ExpressionStatement + +```go +type ExpressionStatement struct { + Expression Expression +} +``` + + +#### func (*ExpressionStatement) Idx0 + +```go +func (self *ExpressionStatement) Idx0() file.Idx +``` + +#### func (*ExpressionStatement) Idx1 + +```go +func (self *ExpressionStatement) Idx1() file.Idx +``` + +#### type ForInStatement + +```go +type ForInStatement struct { + For file.Idx + Into Expression + Source Expression + Body Statement +} +``` + + +#### func (*ForInStatement) Idx0 + +```go +func (self *ForInStatement) Idx0() file.Idx +``` + +#### func (*ForInStatement) Idx1 + +```go +func (self *ForInStatement) Idx1() file.Idx +``` + +#### type ForStatement + +```go +type ForStatement struct { + For file.Idx + Initializer Expression + Update Expression + Test Expression + Body Statement +} +``` + + +#### func (*ForStatement) Idx0 + +```go +func (self *ForStatement) Idx0() file.Idx +``` + +#### func (*ForStatement) Idx1 + +```go +func (self *ForStatement) Idx1() file.Idx +``` + +#### type FunctionDeclaration + +```go +type FunctionDeclaration struct { + Function *FunctionLiteral +} +``` + + +#### type FunctionLiteral + +```go +type FunctionLiteral struct { + Function file.Idx + Name *Identifier + ParameterList *ParameterList + Body Statement + Source string + + DeclarationList []Declaration +} +``` + + +#### func (*FunctionLiteral) Idx0 + +```go +func (self *FunctionLiteral) Idx0() file.Idx +``` + +#### func (*FunctionLiteral) Idx1 + +```go +func (self *FunctionLiteral) Idx1() file.Idx +``` + +#### type Identifier + +```go +type Identifier struct { + Name string + Idx file.Idx +} +``` + + +#### func (*Identifier) Idx0 + +```go +func (self *Identifier) Idx0() file.Idx +``` + +#### func (*Identifier) Idx1 + +```go +func (self *Identifier) Idx1() file.Idx +``` + +#### type IfStatement + +```go +type IfStatement struct { + If file.Idx + Test Expression + Consequent Statement + Alternate Statement +} +``` + + +#### func (*IfStatement) Idx0 + +```go +func (self *IfStatement) Idx0() file.Idx +``` + +#### func (*IfStatement) Idx1 + +```go +func (self *IfStatement) Idx1() file.Idx +``` + +#### type LabelledStatement + +```go +type LabelledStatement struct { + Label *Identifier + Colon file.Idx + Statement Statement +} +``` + + +#### func (*LabelledStatement) Idx0 + +```go +func (self *LabelledStatement) Idx0() file.Idx +``` + +#### func (*LabelledStatement) Idx1 + +```go +func (self *LabelledStatement) Idx1() file.Idx +``` + +#### type NewExpression + +```go +type NewExpression struct { + New file.Idx + Callee Expression + LeftParenthesis file.Idx + ArgumentList []Expression + RightParenthesis file.Idx +} +``` + + +#### func (*NewExpression) Idx0 + +```go +func (self *NewExpression) Idx0() file.Idx +``` + +#### func (*NewExpression) Idx1 + +```go +func (self *NewExpression) Idx1() file.Idx +``` + +#### type Node + +```go +type Node interface { + Idx0() file.Idx // The index of the first character belonging to the node + Idx1() file.Idx // The index of the first character immediately after the node +} +``` + +All nodes implement the Node interface. + +#### type NullLiteral + +```go +type NullLiteral struct { + Idx file.Idx + Literal string +} +``` + + +#### func (*NullLiteral) Idx0 + +```go +func (self *NullLiteral) Idx0() file.Idx +``` + +#### func (*NullLiteral) Idx1 + +```go +func (self *NullLiteral) Idx1() file.Idx +``` + +#### type NumberLiteral + +```go +type NumberLiteral struct { + Idx file.Idx + Literal string + Value interface{} +} +``` + + +#### func (*NumberLiteral) Idx0 + +```go +func (self *NumberLiteral) Idx0() file.Idx +``` + +#### func (*NumberLiteral) Idx1 + +```go +func (self *NumberLiteral) Idx1() file.Idx +``` + +#### type ObjectLiteral + +```go +type ObjectLiteral struct { + LeftBrace file.Idx + RightBrace file.Idx + Value []Property +} +``` + + +#### func (*ObjectLiteral) Idx0 + +```go +func (self *ObjectLiteral) Idx0() file.Idx +``` + +#### func (*ObjectLiteral) Idx1 + +```go +func (self *ObjectLiteral) Idx1() file.Idx +``` + +#### type ParameterList + +```go +type ParameterList struct { + Opening file.Idx + List []*Identifier + Closing file.Idx +} +``` + + +#### type Program + +```go +type Program struct { + Body []Statement + + DeclarationList []Declaration + + File *file.File +} +``` + + +#### func (*Program) Idx0 + +```go +func (self *Program) Idx0() file.Idx +``` + +#### func (*Program) Idx1 + +```go +func (self *Program) Idx1() file.Idx +``` + +#### type Property + +```go +type Property struct { + Key string + Kind string + Value Expression +} +``` + + +#### type RegExpLiteral + +```go +type RegExpLiteral struct { + Idx file.Idx + Literal string + Pattern string + Flags string + Value string +} +``` + + +#### func (*RegExpLiteral) Idx0 + +```go +func (self *RegExpLiteral) Idx0() file.Idx +``` + +#### func (*RegExpLiteral) Idx1 + +```go +func (self *RegExpLiteral) Idx1() file.Idx +``` + +#### type ReturnStatement + +```go +type ReturnStatement struct { + Return file.Idx + Argument Expression +} +``` + + +#### func (*ReturnStatement) Idx0 + +```go +func (self *ReturnStatement) Idx0() file.Idx +``` + +#### func (*ReturnStatement) Idx1 + +```go +func (self *ReturnStatement) Idx1() file.Idx +``` + +#### type SequenceExpression + +```go +type SequenceExpression struct { + Sequence []Expression +} +``` + + +#### func (*SequenceExpression) Idx0 + +```go +func (self *SequenceExpression) Idx0() file.Idx +``` + +#### func (*SequenceExpression) Idx1 + +```go +func (self *SequenceExpression) Idx1() file.Idx +``` + +#### type Statement + +```go +type Statement interface { + Node + // contains filtered or unexported methods +} +``` + +All statement nodes implement the Statement interface. + +#### type StringLiteral + +```go +type StringLiteral struct { + Idx file.Idx + Literal string + Value string +} +``` + + +#### func (*StringLiteral) Idx0 + +```go +func (self *StringLiteral) Idx0() file.Idx +``` + +#### func (*StringLiteral) Idx1 + +```go +func (self *StringLiteral) Idx1() file.Idx +``` + +#### type SwitchStatement + +```go +type SwitchStatement struct { + Switch file.Idx + Discriminant Expression + Default int + Body []*CaseStatement +} +``` + + +#### func (*SwitchStatement) Idx0 + +```go +func (self *SwitchStatement) Idx0() file.Idx +``` + +#### func (*SwitchStatement) Idx1 + +```go +func (self *SwitchStatement) Idx1() file.Idx +``` + +#### type ThisExpression + +```go +type ThisExpression struct { + Idx file.Idx +} +``` + + +#### func (*ThisExpression) Idx0 + +```go +func (self *ThisExpression) Idx0() file.Idx +``` + +#### func (*ThisExpression) Idx1 + +```go +func (self *ThisExpression) Idx1() file.Idx +``` + +#### type ThrowStatement + +```go +type ThrowStatement struct { + Throw file.Idx + Argument Expression +} +``` + + +#### func (*ThrowStatement) Idx0 + +```go +func (self *ThrowStatement) Idx0() file.Idx +``` + +#### func (*ThrowStatement) Idx1 + +```go +func (self *ThrowStatement) Idx1() file.Idx +``` + +#### type TryStatement + +```go +type TryStatement struct { + Try file.Idx + Body Statement + Catch *CatchStatement + Finally Statement +} +``` + + +#### func (*TryStatement) Idx0 + +```go +func (self *TryStatement) Idx0() file.Idx +``` + +#### func (*TryStatement) Idx1 + +```go +func (self *TryStatement) Idx1() file.Idx +``` + +#### type UnaryExpression + +```go +type UnaryExpression struct { + Operator token.Token + Idx file.Idx // If a prefix operation + Operand Expression + Postfix bool +} +``` + + +#### func (*UnaryExpression) Idx0 + +```go +func (self *UnaryExpression) Idx0() file.Idx +``` + +#### func (*UnaryExpression) Idx1 + +```go +func (self *UnaryExpression) Idx1() file.Idx +``` + +#### type VariableDeclaration + +```go +type VariableDeclaration struct { + Var file.Idx + List []*VariableExpression +} +``` + + +#### type VariableExpression + +```go +type VariableExpression struct { + Name string + Idx file.Idx + Initializer Expression +} +``` + + +#### func (*VariableExpression) Idx0 + +```go +func (self *VariableExpression) Idx0() file.Idx +``` + +#### func (*VariableExpression) Idx1 + +```go +func (self *VariableExpression) Idx1() file.Idx +``` + +#### type VariableStatement + +```go +type VariableStatement struct { + Var file.Idx + List []Expression +} +``` + + +#### func (*VariableStatement) Idx0 + +```go +func (self *VariableStatement) Idx0() file.Idx +``` + +#### func (*VariableStatement) Idx1 + +```go +func (self *VariableStatement) Idx1() file.Idx +``` + +#### type WhileStatement + +```go +type WhileStatement struct { + While file.Idx + Test Expression + Body Statement +} +``` + + +#### func (*WhileStatement) Idx0 + +```go +func (self *WhileStatement) Idx0() file.Idx +``` + +#### func (*WhileStatement) Idx1 + +```go +func (self *WhileStatement) Idx1() file.Idx +``` + +#### type WithStatement + +```go +type WithStatement struct { + With file.Idx + Object Expression + Body Statement +} +``` + + +#### func (*WithStatement) Idx0 + +```go +func (self *WithStatement) Idx0() file.Idx +``` + +#### func (*WithStatement) Idx1 + +```go +func (self *WithStatement) Idx1() file.Idx +``` + +-- +**godocdown** http://github.com/robertkrimen/godocdown diff --git a/goja/ast/node.go b/goja/ast/node.go new file mode 100644 index 0000000..3bec89d --- /dev/null +++ b/goja/ast/node.go @@ -0,0 +1,876 @@ +/* +Package ast declares types representing a JavaScript AST. + +# Warning + +The parser and AST interfaces are still works-in-progress (particularly where +node types are concerned) and may change in the future. +*/ +package ast + +import ( + "github.com/dop251/goja/file" + "github.com/dop251/goja/token" + "github.com/dop251/goja/unistring" +) + +type PropertyKind string + +const ( + PropertyKindValue PropertyKind = "value" + PropertyKindGet PropertyKind = "get" + PropertyKindSet PropertyKind = "set" + PropertyKindMethod PropertyKind = "method" +) + +// All nodes implement the Node interface. +type Node interface { + Idx0() file.Idx // The index of the first character belonging to the node + Idx1() file.Idx // The index of the first character immediately after the node +} + +// ========== // +// Expression // +// ========== // + +type ( + // All expression nodes implement the Expression interface. + Expression interface { + Node + _expressionNode() + } + + BindingTarget interface { + Expression + _bindingTarget() + } + + Binding struct { + Target BindingTarget + Initializer Expression + } + + Pattern interface { + BindingTarget + _pattern() + } + + YieldExpression struct { + Yield file.Idx + Argument Expression + Delegate bool + } + + AwaitExpression struct { + Await file.Idx + Argument Expression + } + + ArrayLiteral struct { + LeftBracket file.Idx + RightBracket file.Idx + Value []Expression + } + + ArrayPattern struct { + LeftBracket file.Idx + RightBracket file.Idx + Elements []Expression + Rest Expression + } + + AssignExpression struct { + Operator token.Token + Left Expression + Right Expression + } + + BadExpression struct { + From file.Idx + To file.Idx + } + + BinaryExpression struct { + Operator token.Token + Left Expression + Right Expression + Comparison bool + } + + BooleanLiteral struct { + Idx file.Idx + Literal string + Value bool + } + + BracketExpression struct { + Left Expression + Member Expression + LeftBracket file.Idx + RightBracket file.Idx + } + + CallExpression struct { + Callee Expression + LeftParenthesis file.Idx + ArgumentList []Expression + RightParenthesis file.Idx + } + + ConditionalExpression struct { + Test Expression + Consequent Expression + Alternate Expression + } + + DotExpression struct { + Left Expression + Identifier Identifier + } + + PrivateDotExpression struct { + Left Expression + Identifier PrivateIdentifier + } + + OptionalChain struct { + Expression + } + + Optional struct { + Expression + } + + FunctionLiteral struct { + Function file.Idx + Name *Identifier + ParameterList *ParameterList + Body *BlockStatement + Source string + + DeclarationList []*VariableDeclaration + + Async, Generator bool + } + + ClassLiteral struct { + Class file.Idx + RightBrace file.Idx + Name *Identifier + SuperClass Expression + Body []ClassElement + Source string + } + + ConciseBody interface { + Node + _conciseBody() + } + + ExpressionBody struct { + Expression Expression + } + + ArrowFunctionLiteral struct { + Start file.Idx + ParameterList *ParameterList + Body ConciseBody + Source string + DeclarationList []*VariableDeclaration + Async bool + } + + Identifier struct { + Name unistring.String + Idx file.Idx + } + + PrivateIdentifier struct { + Identifier + } + + NewExpression struct { + New file.Idx + Callee Expression + LeftParenthesis file.Idx + ArgumentList []Expression + RightParenthesis file.Idx + } + + NullLiteral struct { + Idx file.Idx + Literal string + } + + NumberLiteral struct { + Idx file.Idx + Literal string + Value interface{} + } + + ObjectLiteral struct { + LeftBrace file.Idx + RightBrace file.Idx + Value []Property + } + + ObjectPattern struct { + LeftBrace file.Idx + RightBrace file.Idx + Properties []Property + Rest Expression + } + + ParameterList struct { + Opening file.Idx + List []*Binding + Rest Expression + Closing file.Idx + } + + Property interface { + Expression + _property() + } + + PropertyShort struct { + Name Identifier + Initializer Expression + } + + PropertyKeyed struct { + Key Expression + Kind PropertyKind + Value Expression + Computed bool + } + + SpreadElement struct { + Expression + } + + RegExpLiteral struct { + Idx file.Idx + Literal string + Pattern string + Flags string + } + + SequenceExpression struct { + Sequence []Expression + } + + StringLiteral struct { + Idx file.Idx + Literal string + Value unistring.String + } + + TemplateElement struct { + Idx file.Idx + Literal string + Parsed unistring.String + Valid bool + } + + TemplateLiteral struct { + OpenQuote file.Idx + CloseQuote file.Idx + Tag Expression + Elements []*TemplateElement + Expressions []Expression + } + + ThisExpression struct { + Idx file.Idx + } + + SuperExpression struct { + Idx file.Idx + } + + UnaryExpression struct { + Operator token.Token + Idx file.Idx // If a prefix operation + Operand Expression + Postfix bool + } + + MetaProperty struct { + Meta, Property *Identifier + Idx file.Idx + } +) + +// _expressionNode + +func (*ArrayLiteral) _expressionNode() {} +func (*AssignExpression) _expressionNode() {} +func (*YieldExpression) _expressionNode() {} +func (*AwaitExpression) _expressionNode() {} +func (*BadExpression) _expressionNode() {} +func (*BinaryExpression) _expressionNode() {} +func (*BooleanLiteral) _expressionNode() {} +func (*BracketExpression) _expressionNode() {} +func (*CallExpression) _expressionNode() {} +func (*ConditionalExpression) _expressionNode() {} +func (*DotExpression) _expressionNode() {} +func (*PrivateDotExpression) _expressionNode() {} +func (*FunctionLiteral) _expressionNode() {} +func (*ClassLiteral) _expressionNode() {} +func (*ArrowFunctionLiteral) _expressionNode() {} +func (*Identifier) _expressionNode() {} +func (*NewExpression) _expressionNode() {} +func (*NullLiteral) _expressionNode() {} +func (*NumberLiteral) _expressionNode() {} +func (*ObjectLiteral) _expressionNode() {} +func (*RegExpLiteral) _expressionNode() {} +func (*SequenceExpression) _expressionNode() {} +func (*StringLiteral) _expressionNode() {} +func (*TemplateLiteral) _expressionNode() {} +func (*ThisExpression) _expressionNode() {} +func (*SuperExpression) _expressionNode() {} +func (*UnaryExpression) _expressionNode() {} +func (*MetaProperty) _expressionNode() {} +func (*ObjectPattern) _expressionNode() {} +func (*ArrayPattern) _expressionNode() {} +func (*Binding) _expressionNode() {} + +func (*PropertyShort) _expressionNode() {} +func (*PropertyKeyed) _expressionNode() {} + +// ========= // +// Statement // +// ========= // + +type ( + // All statement nodes implement the Statement interface. + Statement interface { + Node + _statementNode() + } + + BadStatement struct { + From file.Idx + To file.Idx + } + + BlockStatement struct { + LeftBrace file.Idx + List []Statement + RightBrace file.Idx + } + + BranchStatement struct { + Idx file.Idx + Token token.Token + Label *Identifier + } + + CaseStatement struct { + Case file.Idx + Test Expression + Consequent []Statement + } + + CatchStatement struct { + Catch file.Idx + Parameter BindingTarget + Body *BlockStatement + } + + DebuggerStatement struct { + Debugger file.Idx + } + + DoWhileStatement struct { + Do file.Idx + Test Expression + Body Statement + RightParenthesis file.Idx + } + + EmptyStatement struct { + Semicolon file.Idx + } + + ExpressionStatement struct { + Expression Expression + } + + ForInStatement struct { + For file.Idx + Into ForInto + Source Expression + Body Statement + } + + ForOfStatement struct { + For file.Idx + Into ForInto + Source Expression + Body Statement + } + + ForStatement struct { + For file.Idx + Initializer ForLoopInitializer + Update Expression + Test Expression + Body Statement + } + + IfStatement struct { + If file.Idx + Test Expression + Consequent Statement + Alternate Statement + } + + LabelledStatement struct { + Label *Identifier + Colon file.Idx + Statement Statement + } + + ReturnStatement struct { + Return file.Idx + Argument Expression + } + + SwitchStatement struct { + Switch file.Idx + Discriminant Expression + Default int + Body []*CaseStatement + RightBrace file.Idx + } + + ThrowStatement struct { + Throw file.Idx + Argument Expression + } + + TryStatement struct { + Try file.Idx + Body *BlockStatement + Catch *CatchStatement + Finally *BlockStatement + } + + VariableStatement struct { + Var file.Idx + List []*Binding + } + + LexicalDeclaration struct { + Idx file.Idx + Token token.Token + List []*Binding + } + + WhileStatement struct { + While file.Idx + Test Expression + Body Statement + } + + WithStatement struct { + With file.Idx + Object Expression + Body Statement + } + + FunctionDeclaration struct { + Function *FunctionLiteral + } + + ClassDeclaration struct { + Class *ClassLiteral + } +) + +// _statementNode + +func (*BadStatement) _statementNode() {} +func (*BlockStatement) _statementNode() {} +func (*BranchStatement) _statementNode() {} +func (*CaseStatement) _statementNode() {} +func (*CatchStatement) _statementNode() {} +func (*DebuggerStatement) _statementNode() {} +func (*DoWhileStatement) _statementNode() {} +func (*EmptyStatement) _statementNode() {} +func (*ExpressionStatement) _statementNode() {} +func (*ForInStatement) _statementNode() {} +func (*ForOfStatement) _statementNode() {} +func (*ForStatement) _statementNode() {} +func (*IfStatement) _statementNode() {} +func (*LabelledStatement) _statementNode() {} +func (*ReturnStatement) _statementNode() {} +func (*SwitchStatement) _statementNode() {} +func (*ThrowStatement) _statementNode() {} +func (*TryStatement) _statementNode() {} +func (*VariableStatement) _statementNode() {} +func (*WhileStatement) _statementNode() {} +func (*WithStatement) _statementNode() {} +func (*LexicalDeclaration) _statementNode() {} +func (*FunctionDeclaration) _statementNode() {} +func (*ClassDeclaration) _statementNode() {} + +// =========== // +// Declaration // +// =========== // + +type ( + VariableDeclaration struct { + Var file.Idx + List []*Binding + } + + ClassElement interface { + Node + _classElement() + } + + FieldDefinition struct { + Idx file.Idx + Key Expression + Initializer Expression + Computed bool + Static bool + } + + MethodDefinition struct { + Idx file.Idx + Key Expression + Kind PropertyKind // "method", "get" or "set" + Body *FunctionLiteral + Computed bool + Static bool + } + + ClassStaticBlock struct { + Static file.Idx + Block *BlockStatement + Source string + DeclarationList []*VariableDeclaration + } +) + +type ( + ForLoopInitializer interface { + Node + _forLoopInitializer() + } + + ForLoopInitializerExpression struct { + Expression Expression + } + + ForLoopInitializerVarDeclList struct { + Var file.Idx + List []*Binding + } + + ForLoopInitializerLexicalDecl struct { + LexicalDeclaration LexicalDeclaration + } + + ForInto interface { + Node + _forInto() + } + + ForIntoVar struct { + Binding *Binding + } + + ForDeclaration struct { + Idx file.Idx + IsConst bool + Target BindingTarget + } + + ForIntoExpression struct { + Expression Expression + } +) + +func (*ForLoopInitializerExpression) _forLoopInitializer() {} +func (*ForLoopInitializerVarDeclList) _forLoopInitializer() {} +func (*ForLoopInitializerLexicalDecl) _forLoopInitializer() {} + +func (*ForIntoVar) _forInto() {} +func (*ForDeclaration) _forInto() {} +func (*ForIntoExpression) _forInto() {} + +func (*ArrayPattern) _pattern() {} +func (*ArrayPattern) _bindingTarget() {} + +func (*ObjectPattern) _pattern() {} +func (*ObjectPattern) _bindingTarget() {} + +func (*BadExpression) _bindingTarget() {} + +func (*PropertyShort) _property() {} +func (*PropertyKeyed) _property() {} +func (*SpreadElement) _property() {} + +func (*Identifier) _bindingTarget() {} + +func (*BlockStatement) _conciseBody() {} +func (*ExpressionBody) _conciseBody() {} + +func (*FieldDefinition) _classElement() {} +func (*MethodDefinition) _classElement() {} +func (*ClassStaticBlock) _classElement() {} + +// ==== // +// Node // +// ==== // + +type Program struct { + Body []Statement + + DeclarationList []*VariableDeclaration + + File *file.File +} + +// ==== // +// Idx0 // +// ==== // + +func (self *ArrayLiteral) Idx0() file.Idx { return self.LeftBracket } +func (self *ArrayPattern) Idx0() file.Idx { return self.LeftBracket } +func (self *YieldExpression) Idx0() file.Idx { return self.Yield } +func (self *AwaitExpression) Idx0() file.Idx { return self.Await } +func (self *ObjectPattern) Idx0() file.Idx { return self.LeftBrace } +func (self *ParameterList) Idx0() file.Idx { return self.Opening } +func (self *AssignExpression) Idx0() file.Idx { return self.Left.Idx0() } +func (self *BadExpression) Idx0() file.Idx { return self.From } +func (self *BinaryExpression) Idx0() file.Idx { return self.Left.Idx0() } +func (self *BooleanLiteral) Idx0() file.Idx { return self.Idx } +func (self *BracketExpression) Idx0() file.Idx { return self.Left.Idx0() } +func (self *CallExpression) Idx0() file.Idx { return self.Callee.Idx0() } +func (self *ConditionalExpression) Idx0() file.Idx { return self.Test.Idx0() } +func (self *DotExpression) Idx0() file.Idx { return self.Left.Idx0() } +func (self *PrivateDotExpression) Idx0() file.Idx { return self.Left.Idx0() } +func (self *FunctionLiteral) Idx0() file.Idx { return self.Function } +func (self *ClassLiteral) Idx0() file.Idx { return self.Class } +func (self *ArrowFunctionLiteral) Idx0() file.Idx { return self.Start } +func (self *Identifier) Idx0() file.Idx { return self.Idx } +func (self *NewExpression) Idx0() file.Idx { return self.New } +func (self *NullLiteral) Idx0() file.Idx { return self.Idx } +func (self *NumberLiteral) Idx0() file.Idx { return self.Idx } +func (self *ObjectLiteral) Idx0() file.Idx { return self.LeftBrace } +func (self *RegExpLiteral) Idx0() file.Idx { return self.Idx } +func (self *SequenceExpression) Idx0() file.Idx { return self.Sequence[0].Idx0() } +func (self *StringLiteral) Idx0() file.Idx { return self.Idx } +func (self *TemplateElement) Idx0() file.Idx { return self.Idx } +func (self *TemplateLiteral) Idx0() file.Idx { return self.OpenQuote } +func (self *ThisExpression) Idx0() file.Idx { return self.Idx } +func (self *SuperExpression) Idx0() file.Idx { return self.Idx } +func (self *UnaryExpression) Idx0() file.Idx { + if self.Postfix { + return self.Operand.Idx0() + } + return self.Idx +} +func (self *MetaProperty) Idx0() file.Idx { return self.Idx } + +func (self *BadStatement) Idx0() file.Idx { return self.From } +func (self *BlockStatement) Idx0() file.Idx { return self.LeftBrace } +func (self *BranchStatement) Idx0() file.Idx { return self.Idx } +func (self *CaseStatement) Idx0() file.Idx { return self.Case } +func (self *CatchStatement) Idx0() file.Idx { return self.Catch } +func (self *DebuggerStatement) Idx0() file.Idx { return self.Debugger } +func (self *DoWhileStatement) Idx0() file.Idx { return self.Do } +func (self *EmptyStatement) Idx0() file.Idx { return self.Semicolon } +func (self *ExpressionStatement) Idx0() file.Idx { return self.Expression.Idx0() } +func (self *ForInStatement) Idx0() file.Idx { return self.For } +func (self *ForOfStatement) Idx0() file.Idx { return self.For } +func (self *ForStatement) Idx0() file.Idx { return self.For } +func (self *IfStatement) Idx0() file.Idx { return self.If } +func (self *LabelledStatement) Idx0() file.Idx { return self.Label.Idx0() } +func (self *Program) Idx0() file.Idx { return self.Body[0].Idx0() } +func (self *ReturnStatement) Idx0() file.Idx { return self.Return } +func (self *SwitchStatement) Idx0() file.Idx { return self.Switch } +func (self *ThrowStatement) Idx0() file.Idx { return self.Throw } +func (self *TryStatement) Idx0() file.Idx { return self.Try } +func (self *VariableStatement) Idx0() file.Idx { return self.Var } +func (self *WhileStatement) Idx0() file.Idx { return self.While } +func (self *WithStatement) Idx0() file.Idx { return self.With } +func (self *LexicalDeclaration) Idx0() file.Idx { return self.Idx } +func (self *FunctionDeclaration) Idx0() file.Idx { return self.Function.Idx0() } +func (self *ClassDeclaration) Idx0() file.Idx { return self.Class.Idx0() } +func (self *Binding) Idx0() file.Idx { return self.Target.Idx0() } + +func (self *ForLoopInitializerExpression) Idx0() file.Idx { return self.Expression.Idx0() } +func (self *ForLoopInitializerVarDeclList) Idx0() file.Idx { return self.List[0].Idx0() } +func (self *ForLoopInitializerLexicalDecl) Idx0() file.Idx { return self.LexicalDeclaration.Idx0() } +func (self *PropertyShort) Idx0() file.Idx { return self.Name.Idx } +func (self *PropertyKeyed) Idx0() file.Idx { return self.Key.Idx0() } +func (self *ExpressionBody) Idx0() file.Idx { return self.Expression.Idx0() } + +func (self *VariableDeclaration) Idx0() file.Idx { return self.Var } +func (self *FieldDefinition) Idx0() file.Idx { return self.Idx } +func (self *MethodDefinition) Idx0() file.Idx { return self.Idx } +func (self *ClassStaticBlock) Idx0() file.Idx { return self.Static } + +func (self *ForDeclaration) Idx0() file.Idx { return self.Idx } +func (self *ForIntoVar) Idx0() file.Idx { return self.Binding.Idx0() } +func (self *ForIntoExpression) Idx0() file.Idx { return self.Expression.Idx0() } + +// ==== // +// Idx1 // +// ==== // + +func (self *ArrayLiteral) Idx1() file.Idx { return self.RightBracket + 1 } +func (self *ArrayPattern) Idx1() file.Idx { return self.RightBracket + 1 } +func (self *AssignExpression) Idx1() file.Idx { return self.Right.Idx1() } +func (self *AwaitExpression) Idx1() file.Idx { return self.Argument.Idx1() } +func (self *BadExpression) Idx1() file.Idx { return self.To } +func (self *BinaryExpression) Idx1() file.Idx { return self.Right.Idx1() } +func (self *BooleanLiteral) Idx1() file.Idx { return file.Idx(int(self.Idx) + len(self.Literal)) } +func (self *BracketExpression) Idx1() file.Idx { return self.RightBracket + 1 } +func (self *CallExpression) Idx1() file.Idx { return self.RightParenthesis + 1 } +func (self *ConditionalExpression) Idx1() file.Idx { return self.Alternate.Idx1() } +func (self *DotExpression) Idx1() file.Idx { return self.Identifier.Idx1() } +func (self *PrivateDotExpression) Idx1() file.Idx { return self.Identifier.Idx1() } +func (self *FunctionLiteral) Idx1() file.Idx { return self.Body.Idx1() } +func (self *ClassLiteral) Idx1() file.Idx { return self.RightBrace + 1 } +func (self *ArrowFunctionLiteral) Idx1() file.Idx { return self.Body.Idx1() } +func (self *Identifier) Idx1() file.Idx { return file.Idx(int(self.Idx) + len(self.Name)) } +func (self *NewExpression) Idx1() file.Idx { + if self.ArgumentList != nil { + return self.RightParenthesis + 1 + } else { + return self.Callee.Idx1() + } +} +func (self *NullLiteral) Idx1() file.Idx { return file.Idx(int(self.Idx) + 4) } // "null" +func (self *NumberLiteral) Idx1() file.Idx { return file.Idx(int(self.Idx) + len(self.Literal)) } +func (self *ObjectLiteral) Idx1() file.Idx { return self.RightBrace + 1 } +func (self *ObjectPattern) Idx1() file.Idx { return self.RightBrace + 1 } +func (self *ParameterList) Idx1() file.Idx { return self.Closing + 1 } +func (self *RegExpLiteral) Idx1() file.Idx { return file.Idx(int(self.Idx) + len(self.Literal)) } +func (self *SequenceExpression) Idx1() file.Idx { return self.Sequence[len(self.Sequence)-1].Idx1() } +func (self *StringLiteral) Idx1() file.Idx { return file.Idx(int(self.Idx) + len(self.Literal)) } +func (self *TemplateElement) Idx1() file.Idx { return file.Idx(int(self.Idx) + len(self.Literal)) } +func (self *TemplateLiteral) Idx1() file.Idx { return self.CloseQuote + 1 } +func (self *ThisExpression) Idx1() file.Idx { return self.Idx + 4 } +func (self *SuperExpression) Idx1() file.Idx { return self.Idx + 5 } +func (self *UnaryExpression) Idx1() file.Idx { + if self.Postfix { + return self.Operand.Idx1() + 2 // ++ -- + } + return self.Operand.Idx1() +} +func (self *MetaProperty) Idx1() file.Idx { + return self.Property.Idx1() +} + +func (self *BadStatement) Idx1() file.Idx { return self.To } +func (self *BlockStatement) Idx1() file.Idx { return self.RightBrace + 1 } +func (self *BranchStatement) Idx1() file.Idx { + if self.Label == nil { + return file.Idx(int(self.Idx) + len(self.Token.String())) + } + return self.Label.Idx1() +} +func (self *CaseStatement) Idx1() file.Idx { return self.Consequent[len(self.Consequent)-1].Idx1() } +func (self *CatchStatement) Idx1() file.Idx { return self.Body.Idx1() } +func (self *DebuggerStatement) Idx1() file.Idx { return self.Debugger + 8 } +func (self *DoWhileStatement) Idx1() file.Idx { return self.RightParenthesis + 1 } +func (self *EmptyStatement) Idx1() file.Idx { return self.Semicolon + 1 } +func (self *ExpressionStatement) Idx1() file.Idx { return self.Expression.Idx1() } +func (self *ForInStatement) Idx1() file.Idx { return self.Body.Idx1() } +func (self *ForOfStatement) Idx1() file.Idx { return self.Body.Idx1() } +func (self *ForStatement) Idx1() file.Idx { return self.Body.Idx1() } +func (self *IfStatement) Idx1() file.Idx { + if self.Alternate != nil { + return self.Alternate.Idx1() + } + return self.Consequent.Idx1() +} +func (self *LabelledStatement) Idx1() file.Idx { return self.Statement.Idx1() } +func (self *Program) Idx1() file.Idx { return self.Body[len(self.Body)-1].Idx1() } +func (self *ReturnStatement) Idx1() file.Idx { + if self.Argument != nil { + return self.Argument.Idx1() + } + return self.Return + 6 +} +func (self *SwitchStatement) Idx1() file.Idx { return self.RightBrace + 1 } +func (self *ThrowStatement) Idx1() file.Idx { return self.Argument.Idx1() } +func (self *TryStatement) Idx1() file.Idx { + if self.Finally != nil { + return self.Finally.Idx1() + } + if self.Catch != nil { + return self.Catch.Idx1() + } + return self.Body.Idx1() +} +func (self *VariableStatement) Idx1() file.Idx { return self.List[len(self.List)-1].Idx1() } +func (self *WhileStatement) Idx1() file.Idx { return self.Body.Idx1() } +func (self *WithStatement) Idx1() file.Idx { return self.Body.Idx1() } +func (self *LexicalDeclaration) Idx1() file.Idx { return self.List[len(self.List)-1].Idx1() } +func (self *FunctionDeclaration) Idx1() file.Idx { return self.Function.Idx1() } +func (self *ClassDeclaration) Idx1() file.Idx { return self.Class.Idx1() } +func (self *Binding) Idx1() file.Idx { + if self.Initializer != nil { + return self.Initializer.Idx1() + } + return self.Target.Idx1() +} + +func (self *ForLoopInitializerExpression) Idx1() file.Idx { return self.Expression.Idx1() } +func (self *ForLoopInitializerVarDeclList) Idx1() file.Idx { return self.List[len(self.List)-1].Idx1() } +func (self *ForLoopInitializerLexicalDecl) Idx1() file.Idx { return self.LexicalDeclaration.Idx1() } + +func (self *PropertyShort) Idx1() file.Idx { + if self.Initializer != nil { + return self.Initializer.Idx1() + } + return self.Name.Idx1() +} + +func (self *PropertyKeyed) Idx1() file.Idx { return self.Value.Idx1() } + +func (self *ExpressionBody) Idx1() file.Idx { return self.Expression.Idx1() } + +func (self *VariableDeclaration) Idx1() file.Idx { + if len(self.List) > 0 { + return self.List[len(self.List)-1].Idx1() + } + + return self.Var + 3 +} + +func (self *FieldDefinition) Idx1() file.Idx { + if self.Initializer != nil { + return self.Initializer.Idx1() + } + return self.Key.Idx1() +} + +func (self *MethodDefinition) Idx1() file.Idx { + return self.Body.Idx1() +} + +func (self *ClassStaticBlock) Idx1() file.Idx { + return self.Block.Idx1() +} + +func (self *YieldExpression) Idx1() file.Idx { + if self.Argument != nil { + return self.Argument.Idx1() + } + return self.Yield + 5 +} + +func (self *ForDeclaration) Idx1() file.Idx { return self.Target.Idx1() } +func (self *ForIntoVar) Idx1() file.Idx { return self.Binding.Idx1() } +func (self *ForIntoExpression) Idx1() file.Idx { return self.Expression.Idx1() } diff --git a/goja/builtin_array.go b/goja/builtin_array.go new file mode 100644 index 0000000..147bf4b --- /dev/null +++ b/goja/builtin_array.go @@ -0,0 +1,1794 @@ +package goja + +import ( + "math" + "sort" + "sync" +) + +func (r *Runtime) newArray(prototype *Object) (a *arrayObject) { + v := &Object{runtime: r} + + a = &arrayObject{} + a.class = classArray + a.val = v + a.extensible = true + v.self = a + a.prototype = prototype + a.init() + return +} + +func (r *Runtime) newArrayObject() *arrayObject { + return r.newArray(r.getArrayPrototype()) +} + +func setArrayValues(a *arrayObject, values []Value) *arrayObject { + a.values = values + a.length = uint32(len(values)) + a.objCount = len(values) + return a +} + +func setArrayLength(a *arrayObject, l int64) *arrayObject { + a.setOwnStr("length", intToValue(l), true) + return a +} + +func arraySpeciesCreate(obj *Object, size int64) *Object { + if isArray(obj) { + v := obj.self.getStr("constructor", nil) + if constructObj, ok := v.(*Object); ok { + v = constructObj.self.getSym(SymSpecies, nil) + if v == _null { + v = nil + } + } + + if v != nil && v != _undefined { + constructObj, _ := v.(*Object) + if constructObj != nil { + if constructor := constructObj.self.assertConstructor(); constructor != nil { + return constructor([]Value{intToValue(size)}, constructObj) + } + } + panic(obj.runtime.NewTypeError("Species is not a constructor")) + } + } + return obj.runtime.newArrayLength(size) +} + +func max(a, b int64) int64 { + if a > b { + return a + } + return b +} + +func min(a, b int64) int64 { + if a < b { + return a + } + return b +} + +func relToIdx(rel, l int64) int64 { + if rel >= 0 { + return min(rel, l) + } + return max(l+rel, 0) +} + +func (r *Runtime) newArrayValues(values []Value) *Object { + return setArrayValues(r.newArrayObject(), values).val +} + +func (r *Runtime) newArrayLength(l int64) *Object { + return setArrayLength(r.newArrayObject(), l).val +} + +func (r *Runtime) builtin_newArray(args []Value, proto *Object) *Object { + l := len(args) + if l == 1 { + if al, ok := args[0].(valueInt); ok { + return setArrayLength(r.newArray(proto), int64(al)).val + } else if f, ok := args[0].(valueFloat); ok { + al := int64(f) + if float64(al) == float64(f) { + return r.newArrayLength(al) + } else { + panic(r.newError(r.getRangeError(), "Invalid array length")) + } + } + return setArrayValues(r.newArray(proto), []Value{args[0]}).val + } else { + argsCopy := make([]Value, l) + copy(argsCopy, args) + return setArrayValues(r.newArray(proto), argsCopy).val + } +} + +func (r *Runtime) generic_push(obj *Object, call FunctionCall) Value { + l := toLength(obj.self.getStr("length", nil)) + nl := l + int64(len(call.Arguments)) + if nl >= maxInt { + r.typeErrorResult(true, "Invalid array length") + panic("unreachable") + } + for i, arg := range call.Arguments { + obj.self.setOwnIdx(valueInt(l+int64(i)), arg, true) + } + n := valueInt(nl) + obj.self.setOwnStr("length", n, true) + return n +} + +func (r *Runtime) arrayproto_push(call FunctionCall) Value { + obj := call.This.ToObject(r) + return r.generic_push(obj, call) +} + +func (r *Runtime) arrayproto_pop_generic(obj *Object) Value { + l := toLength(obj.self.getStr("length", nil)) + if l == 0 { + obj.self.setOwnStr("length", intToValue(0), true) + return _undefined + } + idx := valueInt(l - 1) + val := obj.self.getIdx(idx, nil) + obj.self.deleteIdx(idx, true) + obj.self.setOwnStr("length", idx, true) + return val +} + +func (r *Runtime) arrayproto_pop(call FunctionCall) Value { + obj := call.This.ToObject(r) + if a, ok := obj.self.(*arrayObject); ok { + l := a.length + var val Value + if l > 0 { + l-- + if l < uint32(len(a.values)) { + val = a.values[l] + } + if val == nil { + // optimisation bail-out + return r.arrayproto_pop_generic(obj) + } + if _, ok := val.(*valueProperty); ok { + // optimisation bail-out + return r.arrayproto_pop_generic(obj) + } + //a._setLengthInt(l, false) + a.values[l] = nil + a.values = a.values[:l] + } else { + val = _undefined + } + if a.lengthProp.writable { + a.length = l + } else { + a.setLength(0, true) // will throw + } + return val + } else { + return r.arrayproto_pop_generic(obj) + } +} + +func (r *Runtime) arrayproto_join(call FunctionCall) Value { + o := call.This.ToObject(r) + l := int(toLength(o.self.getStr("length", nil))) + var sep String + if s := call.Argument(0); s != _undefined { + sep = s.toString() + } else { + sep = asciiString(",") + } + if l == 0 { + return stringEmpty + } + + var buf StringBuilder + + element0 := o.self.getIdx(valueInt(0), nil) + if element0 != nil && element0 != _undefined && element0 != _null { + buf.WriteString(element0.toString()) + } + + for i := 1; i < l; i++ { + buf.WriteString(sep) + element := o.self.getIdx(valueInt(int64(i)), nil) + if element != nil && element != _undefined && element != _null { + buf.WriteString(element.toString()) + } + } + + return buf.String() +} + +func (r *Runtime) arrayproto_toString(call FunctionCall) Value { + array := call.This.ToObject(r) + var toString func() Value + switch a := array.self.(type) { + case *objectGoSliceReflect: + toString = a.toString + case *objectGoArrayReflect: + toString = a.toString + } + if toString != nil { + return toString() + } + f := array.self.getStr("join", nil) + if fObj, ok := f.(*Object); ok { + if fcall, ok := fObj.self.assertCallable(); ok { + return fcall(FunctionCall{ + This: array, + }) + } + } + return r.objectproto_toString(FunctionCall{ + This: array, + }) +} + +func (r *Runtime) writeItemLocaleString(item Value, buf *StringBuilder) { + if item != nil && item != _undefined && item != _null { + if f, ok := r.getVStr(item, "toLocaleString").(*Object); ok { + if c, ok := f.self.assertCallable(); ok { + strVal := c(FunctionCall{ + This: item, + }) + buf.WriteString(strVal.toString()) + return + } + } + r.typeErrorResult(true, "Property 'toLocaleString' of object %s is not a function", item) + } +} + +func (r *Runtime) arrayproto_toLocaleString(call FunctionCall) Value { + array := call.This.ToObject(r) + var buf StringBuilder + if a := r.checkStdArrayObj(array); a != nil { + for i, item := range a.values { + if i > 0 { + buf.WriteRune(',') + } + r.writeItemLocaleString(item, &buf) + } + } else { + length := toLength(array.self.getStr("length", nil)) + for i := int64(0); i < length; i++ { + if i > 0 { + buf.WriteRune(',') + } + item := array.self.getIdx(valueInt(i), nil) + r.writeItemLocaleString(item, &buf) + } + } + + return buf.String() +} + +func isConcatSpreadable(obj *Object) bool { + spreadable := obj.self.getSym(SymIsConcatSpreadable, nil) + if spreadable != nil && spreadable != _undefined { + return spreadable.ToBoolean() + } + return isArray(obj) +} + +func (r *Runtime) arrayproto_concat_append(a *Object, item Value) { + aLength := toLength(a.self.getStr("length", nil)) + if obj, ok := item.(*Object); ok && isConcatSpreadable(obj) { + length := toLength(obj.self.getStr("length", nil)) + if aLength+length >= maxInt { + panic(r.NewTypeError("Invalid array length")) + } + for i := int64(0); i < length; i++ { + v := obj.self.getIdx(valueInt(i), nil) + if v != nil { + createDataPropertyOrThrow(a, intToValue(aLength), v) + } + aLength++ + } + } else { + createDataPropertyOrThrow(a, intToValue(aLength), item) + aLength++ + } + a.self.setOwnStr("length", intToValue(aLength), true) +} + +func (r *Runtime) arrayproto_concat(call FunctionCall) Value { + obj := call.This.ToObject(r) + a := arraySpeciesCreate(obj, 0) + r.arrayproto_concat_append(a, call.This.ToObject(r)) + for _, item := range call.Arguments { + r.arrayproto_concat_append(a, item) + } + return a +} + +func (r *Runtime) arrayproto_slice(call FunctionCall) Value { + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + start := relToIdx(call.Argument(0).ToInteger(), length) + var end int64 + if endArg := call.Argument(1); endArg != _undefined { + end = endArg.ToInteger() + } else { + end = length + } + end = relToIdx(end, length) + + count := end - start + if count < 0 { + count = 0 + } + + a := arraySpeciesCreate(o, count) + if src := r.checkStdArrayObj(o); src != nil { + if dst := r.checkStdArrayObjWithProto(a); dst != nil { + values := make([]Value, count) + copy(values, src.values[start:]) + setArrayValues(dst, values) + return a + } + } + + n := int64(0) + for start < end { + p := o.self.getIdx(valueInt(start), nil) + if p != nil { + createDataPropertyOrThrow(a, valueInt(n), p) + } + start++ + n++ + } + return a +} + +func (r *Runtime) arrayproto_sort(call FunctionCall) Value { + o := call.This.ToObject(r) + + var compareFn func(FunctionCall) Value + arg := call.Argument(0) + if arg != _undefined { + if arg, ok := call.Argument(0).(*Object); ok { + compareFn, _ = arg.self.assertCallable() + } + if compareFn == nil { + panic(r.NewTypeError("The comparison function must be either a function or undefined")) + } + } + + var s sortable + if r.checkStdArrayObj(o) != nil { + s = o.self + } else if _, ok := o.self.(reflectValueWrapper); ok { + s = o.self + } + + if s != nil { + ctx := arraySortCtx{ + obj: s, + compare: compareFn, + } + + sort.Stable(&ctx) + } else { + length := toLength(o.self.getStr("length", nil)) + a := make([]Value, 0, length) + for i := int64(0); i < length; i++ { + idx := valueInt(i) + if o.self.hasPropertyIdx(idx) { + a = append(a, nilSafe(o.self.getIdx(idx, nil))) + } + } + ar := r.newArrayValues(a) + ctx := arraySortCtx{ + obj: ar.self, + compare: compareFn, + } + + sort.Stable(&ctx) + for i := 0; i < len(a); i++ { + o.self.setOwnIdx(valueInt(i), a[i], true) + } + for i := int64(len(a)); i < length; i++ { + o.self.deleteIdx(valueInt(i), true) + } + } + return o +} + +func (r *Runtime) arrayproto_splice(call FunctionCall) Value { + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + actualStart := relToIdx(call.Argument(0).ToInteger(), length) + var actualDeleteCount int64 + switch len(call.Arguments) { + case 0: + case 1: + actualDeleteCount = length - actualStart + default: + actualDeleteCount = min(max(call.Argument(1).ToInteger(), 0), length-actualStart) + } + itemCount := max(int64(len(call.Arguments)-2), 0) + newLength := length - actualDeleteCount + itemCount + if newLength >= maxInt { + panic(r.NewTypeError("Invalid array length")) + } + a := arraySpeciesCreate(o, actualDeleteCount) + if src := r.checkStdArrayObj(o); src != nil { + if dst := r.checkStdArrayObjWithProto(a); dst != nil { + values := make([]Value, actualDeleteCount) + copy(values, src.values[actualStart:]) + setArrayValues(dst, values) + } else { + for k := int64(0); k < actualDeleteCount; k++ { + createDataPropertyOrThrow(a, intToValue(k), src.values[k+actualStart]) + } + a.self.setOwnStr("length", intToValue(actualDeleteCount), true) + } + var values []Value + if itemCount < actualDeleteCount { + values = src.values + copy(values[actualStart+itemCount:], values[actualStart+actualDeleteCount:]) + tail := values[newLength:] + for k := range tail { + tail[k] = nil + } + values = values[:newLength] + } else if itemCount > actualDeleteCount { + if int64(cap(src.values)) >= newLength { + values = src.values[:newLength] + copy(values[actualStart+itemCount:], values[actualStart+actualDeleteCount:length]) + } else { + values = make([]Value, newLength) + copy(values, src.values[:actualStart]) + copy(values[actualStart+itemCount:], src.values[actualStart+actualDeleteCount:]) + } + } else { + values = src.values + } + if itemCount > 0 { + copy(values[actualStart:], call.Arguments[2:]) + } + src.values = values + src.objCount = len(values) + } else { + for k := int64(0); k < actualDeleteCount; k++ { + from := valueInt(k + actualStart) + if o.self.hasPropertyIdx(from) { + createDataPropertyOrThrow(a, valueInt(k), nilSafe(o.self.getIdx(from, nil))) + } + } + + if itemCount < actualDeleteCount { + for k := actualStart; k < length-actualDeleteCount; k++ { + from := valueInt(k + actualDeleteCount) + to := valueInt(k + itemCount) + if o.self.hasPropertyIdx(from) { + o.self.setOwnIdx(to, nilSafe(o.self.getIdx(from, nil)), true) + } else { + o.self.deleteIdx(to, true) + } + } + + for k := length; k > length-actualDeleteCount+itemCount; k-- { + o.self.deleteIdx(valueInt(k-1), true) + } + } else if itemCount > actualDeleteCount { + for k := length - actualDeleteCount; k > actualStart; k-- { + from := valueInt(k + actualDeleteCount - 1) + to := valueInt(k + itemCount - 1) + if o.self.hasPropertyIdx(from) { + o.self.setOwnIdx(to, nilSafe(o.self.getIdx(from, nil)), true) + } else { + o.self.deleteIdx(to, true) + } + } + } + + if itemCount > 0 { + for i, item := range call.Arguments[2:] { + o.self.setOwnIdx(valueInt(actualStart+int64(i)), item, true) + } + } + } + + o.self.setOwnStr("length", intToValue(newLength), true) + + return a +} + +func (r *Runtime) arrayproto_unshift(call FunctionCall) Value { + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + argCount := int64(len(call.Arguments)) + newLen := intToValue(length + argCount) + if argCount > 0 { + newSize := length + argCount + if newSize >= maxInt { + panic(r.NewTypeError("Invalid array length")) + } + if arr := r.checkStdArrayObjWithProto(o); arr != nil && newSize < math.MaxUint32 { + if int64(cap(arr.values)) >= newSize { + arr.values = arr.values[:newSize] + copy(arr.values[argCount:], arr.values[:length]) + } else { + values := make([]Value, newSize) + copy(values[argCount:], arr.values) + arr.values = values + } + copy(arr.values, call.Arguments) + arr.objCount = int(arr.length) + } else { + for k := length - 1; k >= 0; k-- { + from := valueInt(k) + to := valueInt(k + argCount) + if o.self.hasPropertyIdx(from) { + o.self.setOwnIdx(to, nilSafe(o.self.getIdx(from, nil)), true) + } else { + o.self.deleteIdx(to, true) + } + } + + for k, arg := range call.Arguments { + o.self.setOwnIdx(valueInt(int64(k)), arg, true) + } + } + } + + o.self.setOwnStr("length", newLen, true) + return newLen +} + +func (r *Runtime) arrayproto_at(call FunctionCall) Value { + o := call.This.ToObject(r) + idx := call.Argument(0).ToInteger() + length := toLength(o.self.getStr("length", nil)) + if idx < 0 { + idx = length + idx + } + if idx >= length || idx < 0 { + return _undefined + } + i := valueInt(idx) + if o.self.hasPropertyIdx(i) { + return o.self.getIdx(i, nil) + } + return _undefined +} + +func (r *Runtime) arrayproto_indexOf(call FunctionCall) Value { + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + if length == 0 { + return intToValue(-1) + } + + n := call.Argument(1).ToInteger() + if n >= length { + return intToValue(-1) + } + + if n < 0 { + n = max(length+n, 0) + } + + searchElement := call.Argument(0) + + if arr := r.checkStdArrayObj(o); arr != nil { + for i, val := range arr.values[n:] { + if searchElement.StrictEquals(val) { + return intToValue(n + int64(i)) + } + } + return intToValue(-1) + } + + for ; n < length; n++ { + idx := valueInt(n) + if o.self.hasPropertyIdx(idx) { + if val := o.self.getIdx(idx, nil); val != nil { + if searchElement.StrictEquals(val) { + return idx + } + } + } + } + + return intToValue(-1) +} + +func (r *Runtime) arrayproto_includes(call FunctionCall) Value { + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + if length == 0 { + return valueFalse + } + + n := call.Argument(1).ToInteger() + if n >= length { + return valueFalse + } + + if n < 0 { + n = max(length+n, 0) + } + + searchElement := call.Argument(0) + if searchElement == _negativeZero { + searchElement = _positiveZero + } + + if arr := r.checkStdArrayObj(o); arr != nil { + for _, val := range arr.values[n:] { + if searchElement.SameAs(val) { + return valueTrue + } + } + return valueFalse + } + + for ; n < length; n++ { + idx := valueInt(n) + val := nilSafe(o.self.getIdx(idx, nil)) + if searchElement.SameAs(val) { + return valueTrue + } + } + + return valueFalse +} + +func (r *Runtime) arrayproto_lastIndexOf(call FunctionCall) Value { + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + if length == 0 { + return intToValue(-1) + } + + var fromIndex int64 + + if len(call.Arguments) < 2 { + fromIndex = length - 1 + } else { + fromIndex = call.Argument(1).ToInteger() + if fromIndex >= 0 { + fromIndex = min(fromIndex, length-1) + } else { + fromIndex += length + } + } + + searchElement := call.Argument(0) + + if arr := r.checkStdArrayObj(o); arr != nil { + vals := arr.values + for k := fromIndex; k >= 0; k-- { + if v := vals[k]; v != nil && searchElement.StrictEquals(v) { + return intToValue(k) + } + } + return intToValue(-1) + } + + for k := fromIndex; k >= 0; k-- { + idx := valueInt(k) + if o.self.hasPropertyIdx(idx) { + if val := o.self.getIdx(idx, nil); val != nil { + if searchElement.StrictEquals(val) { + return idx + } + } + } + } + + return intToValue(-1) +} + +func (r *Runtime) arrayproto_every(call FunctionCall) Value { + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, o}, + } + for k := int64(0); k < length; k++ { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { + fc.Arguments[0] = val + fc.Arguments[1] = idx + if !callbackFn(fc).ToBoolean() { + return valueFalse + } + } + } + return valueTrue +} + +func (r *Runtime) arrayproto_some(call FunctionCall) Value { + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, o}, + } + for k := int64(0); k < length; k++ { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { + fc.Arguments[0] = val + fc.Arguments[1] = idx + if callbackFn(fc).ToBoolean() { + return valueTrue + } + } + } + return valueFalse +} + +func (r *Runtime) arrayproto_forEach(call FunctionCall) Value { + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, o}, + } + for k := int64(0); k < length; k++ { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { + fc.Arguments[0] = val + fc.Arguments[1] = idx + callbackFn(fc) + } + } + return _undefined +} + +func (r *Runtime) arrayproto_map(call FunctionCall) Value { + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, o}, + } + a := arraySpeciesCreate(o, length) + if _, stdSrc := o.self.(*arrayObject); stdSrc { + if arr, ok := a.self.(*arrayObject); ok { + values := make([]Value, length) + for k := int64(0); k < length; k++ { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { + fc.Arguments[0] = val + fc.Arguments[1] = idx + values[k] = callbackFn(fc) + } + } + setArrayValues(arr, values) + return a + } + } + for k := int64(0); k < length; k++ { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { + fc.Arguments[0] = val + fc.Arguments[1] = idx + createDataPropertyOrThrow(a, idx, callbackFn(fc)) + } + } + return a +} + +func (r *Runtime) arrayproto_filter(call FunctionCall) Value { + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + callbackFn := call.Argument(0).ToObject(r) + if callbackFn, ok := callbackFn.self.assertCallable(); ok { + a := arraySpeciesCreate(o, 0) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, o}, + } + if _, stdSrc := o.self.(*arrayObject); stdSrc { + if arr := r.checkStdArrayObj(a); arr != nil { + var values []Value + for k := int64(0); k < length; k++ { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { + fc.Arguments[0] = val + fc.Arguments[1] = idx + if callbackFn(fc).ToBoolean() { + values = append(values, val) + } + } + } + setArrayValues(arr, values) + return a + } + } + + to := int64(0) + for k := int64(0); k < length; k++ { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { + fc.Arguments[0] = val + fc.Arguments[1] = idx + if callbackFn(fc).ToBoolean() { + createDataPropertyOrThrow(a, intToValue(to), val) + to++ + } + } + } + return a + } else { + r.typeErrorResult(true, "%s is not a function", call.Argument(0)) + } + panic("unreachable") +} + +func (r *Runtime) arrayproto_reduce(call FunctionCall) Value { + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + callbackFn := call.Argument(0).ToObject(r) + if callbackFn, ok := callbackFn.self.assertCallable(); ok { + fc := FunctionCall{ + This: _undefined, + Arguments: []Value{nil, nil, nil, o}, + } + + var k int64 + + if len(call.Arguments) >= 2 { + fc.Arguments[0] = call.Argument(1) + } else { + for ; k < length; k++ { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { + fc.Arguments[0] = val + break + } + } + if fc.Arguments[0] == nil { + r.typeErrorResult(true, "No initial value") + panic("unreachable") + } + k++ + } + + for ; k < length; k++ { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { + fc.Arguments[1] = val + fc.Arguments[2] = idx + fc.Arguments[0] = callbackFn(fc) + } + } + return fc.Arguments[0] + } else { + r.typeErrorResult(true, "%s is not a function", call.Argument(0)) + } + panic("unreachable") +} + +func (r *Runtime) arrayproto_reduceRight(call FunctionCall) Value { + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + callbackFn := call.Argument(0).ToObject(r) + if callbackFn, ok := callbackFn.self.assertCallable(); ok { + fc := FunctionCall{ + This: _undefined, + Arguments: []Value{nil, nil, nil, o}, + } + + k := length - 1 + + if len(call.Arguments) >= 2 { + fc.Arguments[0] = call.Argument(1) + } else { + for ; k >= 0; k-- { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { + fc.Arguments[0] = val + break + } + } + if fc.Arguments[0] == nil { + r.typeErrorResult(true, "No initial value") + panic("unreachable") + } + k-- + } + + for ; k >= 0; k-- { + idx := valueInt(k) + if val := o.self.getIdx(idx, nil); val != nil { + fc.Arguments[1] = val + fc.Arguments[2] = idx + fc.Arguments[0] = callbackFn(fc) + } + } + return fc.Arguments[0] + } else { + r.typeErrorResult(true, "%s is not a function", call.Argument(0)) + } + panic("unreachable") +} + +func arrayproto_reverse_generic_step(o *Object, lower, upper int64) { + lowerP := valueInt(lower) + upperP := valueInt(upper) + var lowerValue, upperValue Value + lowerExists := o.self.hasPropertyIdx(lowerP) + if lowerExists { + lowerValue = nilSafe(o.self.getIdx(lowerP, nil)) + } + upperExists := o.self.hasPropertyIdx(upperP) + if upperExists { + upperValue = nilSafe(o.self.getIdx(upperP, nil)) + } + if lowerExists && upperExists { + o.self.setOwnIdx(lowerP, upperValue, true) + o.self.setOwnIdx(upperP, lowerValue, true) + } else if !lowerExists && upperExists { + o.self.setOwnIdx(lowerP, upperValue, true) + o.self.deleteIdx(upperP, true) + } else if lowerExists && !upperExists { + o.self.deleteIdx(lowerP, true) + o.self.setOwnIdx(upperP, lowerValue, true) + } +} + +func (r *Runtime) arrayproto_reverse_generic(o *Object, start int64) { + l := toLength(o.self.getStr("length", nil)) + middle := l / 2 + for lower := start; lower != middle; lower++ { + arrayproto_reverse_generic_step(o, lower, l-lower-1) + } +} + +func (r *Runtime) arrayproto_reverse(call FunctionCall) Value { + o := call.This.ToObject(r) + if a := r.checkStdArrayObj(o); a != nil { + l := len(a.values) + middle := l / 2 + for lower := 0; lower != middle; lower++ { + upper := l - lower - 1 + a.values[lower], a.values[upper] = a.values[upper], a.values[lower] + } + //TODO: go arrays + } else { + r.arrayproto_reverse_generic(o, 0) + } + return o +} + +func (r *Runtime) arrayproto_shift(call FunctionCall) Value { + o := call.This.ToObject(r) + if a := r.checkStdArrayObjWithProto(o); a != nil { + if len(a.values) == 0 { + if !a.lengthProp.writable { + a.setLength(0, true) // will throw + } + return _undefined + } + first := a.values[0] + copy(a.values, a.values[1:]) + a.values[len(a.values)-1] = nil + a.values = a.values[:len(a.values)-1] + a.length-- + return first + } + length := toLength(o.self.getStr("length", nil)) + if length == 0 { + o.self.setOwnStr("length", intToValue(0), true) + return _undefined + } + first := o.self.getIdx(valueInt(0), nil) + for i := int64(1); i < length; i++ { + idxFrom := valueInt(i) + idxTo := valueInt(i - 1) + if o.self.hasPropertyIdx(idxFrom) { + o.self.setOwnIdx(idxTo, nilSafe(o.self.getIdx(idxFrom, nil)), true) + } else { + o.self.deleteIdx(idxTo, true) + } + } + + lv := valueInt(length - 1) + o.self.deleteIdx(lv, true) + o.self.setOwnStr("length", lv, true) + + return first +} + +func (r *Runtime) arrayproto_values(call FunctionCall) Value { + return r.createArrayIterator(call.This.ToObject(r), iterationKindValue) +} + +func (r *Runtime) arrayproto_keys(call FunctionCall) Value { + return r.createArrayIterator(call.This.ToObject(r), iterationKindKey) +} + +func (r *Runtime) arrayproto_copyWithin(call FunctionCall) Value { + o := call.This.ToObject(r) + l := toLength(o.self.getStr("length", nil)) + var relEnd, dir int64 + to := relToIdx(call.Argument(0).ToInteger(), l) + from := relToIdx(call.Argument(1).ToInteger(), l) + if end := call.Argument(2); end != _undefined { + relEnd = end.ToInteger() + } else { + relEnd = l + } + final := relToIdx(relEnd, l) + count := min(final-from, l-to) + if arr := r.checkStdArrayObj(o); arr != nil { + if count > 0 { + copy(arr.values[to:to+count], arr.values[from:from+count]) + } + return o + } + if from < to && to < from+count { + dir = -1 + from = from + count - 1 + to = to + count - 1 + } else { + dir = 1 + } + for count > 0 { + if o.self.hasPropertyIdx(valueInt(from)) { + o.self.setOwnIdx(valueInt(to), nilSafe(o.self.getIdx(valueInt(from), nil)), true) + } else { + o.self.deleteIdx(valueInt(to), true) + } + from += dir + to += dir + count-- + } + + return o +} + +func (r *Runtime) arrayproto_entries(call FunctionCall) Value { + return r.createArrayIterator(call.This.ToObject(r), iterationKindKeyValue) +} + +func (r *Runtime) arrayproto_fill(call FunctionCall) Value { + o := call.This.ToObject(r) + l := toLength(o.self.getStr("length", nil)) + k := relToIdx(call.Argument(1).ToInteger(), l) + var relEnd int64 + if endArg := call.Argument(2); endArg != _undefined { + relEnd = endArg.ToInteger() + } else { + relEnd = l + } + final := relToIdx(relEnd, l) + value := call.Argument(0) + if arr := r.checkStdArrayObj(o); arr != nil { + for ; k < final; k++ { + arr.values[k] = value + } + } else { + for ; k < final; k++ { + o.self.setOwnIdx(valueInt(k), value, true) + } + } + return o +} + +func (r *Runtime) arrayproto_find(call FunctionCall) Value { + o := call.This.ToObject(r) + l := toLength(o.self.getStr("length", nil)) + predicate := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, o}, + } + for k := int64(0); k < l; k++ { + idx := valueInt(k) + kValue := o.self.getIdx(idx, nil) + fc.Arguments[0], fc.Arguments[1] = kValue, idx + if predicate(fc).ToBoolean() { + return kValue + } + } + + return _undefined +} + +func (r *Runtime) arrayproto_findIndex(call FunctionCall) Value { + o := call.This.ToObject(r) + l := toLength(o.self.getStr("length", nil)) + predicate := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, o}, + } + for k := int64(0); k < l; k++ { + idx := valueInt(k) + kValue := o.self.getIdx(idx, nil) + fc.Arguments[0], fc.Arguments[1] = kValue, idx + if predicate(fc).ToBoolean() { + return idx + } + } + + return intToValue(-1) +} + +func (r *Runtime) arrayproto_findLast(call FunctionCall) Value { + o := call.This.ToObject(r) + l := toLength(o.self.getStr("length", nil)) + predicate := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, o}, + } + for k := int64(l - 1); k >= 0; k-- { + idx := valueInt(k) + kValue := o.self.getIdx(idx, nil) + fc.Arguments[0], fc.Arguments[1] = kValue, idx + if predicate(fc).ToBoolean() { + return kValue + } + } + + return _undefined +} + +func (r *Runtime) arrayproto_findLastIndex(call FunctionCall) Value { + o := call.This.ToObject(r) + l := toLength(o.self.getStr("length", nil)) + predicate := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, o}, + } + for k := int64(l - 1); k >= 0; k-- { + idx := valueInt(k) + kValue := o.self.getIdx(idx, nil) + fc.Arguments[0], fc.Arguments[1] = kValue, idx + if predicate(fc).ToBoolean() { + return idx + } + } + + return intToValue(-1) +} + +func (r *Runtime) arrayproto_flat(call FunctionCall) Value { + o := call.This.ToObject(r) + l := toLength(o.self.getStr("length", nil)) + depthNum := int64(1) + if len(call.Arguments) > 0 { + depthNum = call.Argument(0).ToInteger() + } + a := arraySpeciesCreate(o, 0) + r.flattenIntoArray(a, o, l, 0, depthNum, nil, nil) + return a +} + +func (r *Runtime) flattenIntoArray(target, source *Object, sourceLen, start, depth int64, mapperFunction func(FunctionCall) Value, thisArg Value) int64 { + targetIndex, sourceIndex := start, int64(0) + for sourceIndex < sourceLen { + p := intToValue(sourceIndex) + if source.hasProperty(p.toString()) { + element := nilSafe(source.get(p, source)) + if mapperFunction != nil { + element = mapperFunction(FunctionCall{ + This: thisArg, + Arguments: []Value{element, p, source}, + }) + } + var elementArray *Object + if depth > 0 { + if elementObj, ok := element.(*Object); ok && isArray(elementObj) { + elementArray = elementObj + } + } + if elementArray != nil { + elementLen := toLength(elementArray.self.getStr("length", nil)) + targetIndex = r.flattenIntoArray(target, elementArray, elementLen, targetIndex, depth-1, nil, nil) + } else { + if targetIndex >= maxInt-1 { + panic(r.NewTypeError("Invalid array length")) + } + createDataPropertyOrThrow(target, intToValue(targetIndex), element) + targetIndex++ + } + } + sourceIndex++ + } + return targetIndex +} + +func (r *Runtime) arrayproto_flatMap(call FunctionCall) Value { + o := call.This.ToObject(r) + l := toLength(o.self.getStr("length", nil)) + callbackFn := r.toCallable(call.Argument(0)) + thisArg := Undefined() + if len(call.Arguments) > 1 { + thisArg = call.Argument(1) + } + a := arraySpeciesCreate(o, 0) + r.flattenIntoArray(a, o, l, 0, 1, callbackFn, thisArg) + return a +} + +func (r *Runtime) arrayproto_with(call FunctionCall) Value { + o := call.This.ToObject(r) + relativeIndex := call.Argument(0).ToInteger() + value := call.Argument(1) + length := toLength(o.self.getStr("length", nil)) + + actualIndex := int64(0) + if relativeIndex >= 0 { + actualIndex = relativeIndex + } else { + actualIndex = length + relativeIndex + } + if actualIndex >= length || actualIndex < 0 { + panic(r.newError(r.getRangeError(), "Invalid index %s", call.Argument(0).String())) + } + + if src := r.checkStdArrayObj(o); src != nil { + a := make([]Value, 0, length) + for k := int64(0); k < length; k++ { + pk := valueInt(k) + var fromValue Value + if k == actualIndex { + fromValue = value + } else { + fromValue = src.values[pk] + } + a = append(a, fromValue) + } + return r.newArrayValues(a) + } else { + a := r.newArrayLength(length) + for k := int64(0); k < length; k++ { + pk := valueInt(k) + var fromValue Value + if k == actualIndex { + fromValue = value + } else { + fromValue = o.self.getIdx(pk, nil) + } + createDataPropertyOrThrow(a, pk, fromValue) + } + return a + } +} + +func (r *Runtime) arrayproto_toReversed(call FunctionCall) Value { + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + + if src := r.checkStdArrayObj(o); src != nil { + a := make([]Value, 0, length) + for k := int64(0); k < length; k++ { + from := valueInt(length - k - 1) + fromValue := src.values[from] + a = append(a, fromValue) + } + return r.newArrayValues(a) + } else { + a := r.newArrayLength(length) + for k := int64(0); k < length; k++ { + pk := valueInt(k) + from := valueInt(length - k - 1) + fromValue := o.self.getIdx(from, nil) + createDataPropertyOrThrow(a, pk, fromValue) + } + return a + } +} + +func (r *Runtime) arrayproto_toSorted(call FunctionCall) Value { + var compareFn func(FunctionCall) Value + arg := call.Argument(0) + if arg != _undefined { + if arg, ok := arg.(*Object); ok { + compareFn, _ = arg.self.assertCallable() + } + if compareFn == nil { + panic(r.NewTypeError("The comparison function must be either a function or undefined")) + } + } + + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + if length >= math.MaxUint32 { + panic(r.newError(r.getRangeError(), "Invalid array length")) + } + var a []Value + + if src := r.checkStdArrayObj(o); src != nil { + a = make([]Value, length) + copy(a, src.values) + } else { + a = make([]Value, 0, length) + for i := int64(0); i < length; i++ { + idx := valueInt(i) + a = append(a, nilSafe(o.self.getIdx(idx, nil))) + } + } + + ar := r.newArrayValues(a) + ctx := arraySortCtx{ + obj: ar.self, + compare: compareFn, + } + + sort.Stable(&ctx) + return ar +} + +func (r *Runtime) arrayproto_toSpliced(call FunctionCall) Value { + o := call.This.ToObject(r) + length := toLength(o.self.getStr("length", nil)) + actualStart := relToIdx(call.Argument(0).ToInteger(), length) + var actualSkipCount int64 + if len(call.Arguments) == 1 { + actualSkipCount = length - actualStart + } else if len(call.Arguments) > 1 { + actualSkipCount = min(max(call.Argument(1).ToInteger(), 0), length-actualStart) + } + itemCount := max(int64(len(call.Arguments)-2), 0) + newLength := length - actualSkipCount + itemCount + if newLength >= maxInt { + panic(r.NewTypeError("Invalid array length")) + } + + if src := r.checkStdArrayObj(o); src != nil { + var values []Value + if itemCount == actualSkipCount { + values = make([]Value, len(src.values)) + copy(values, src.values) + } else { + values = make([]Value, newLength) + copy(values, src.values[:actualStart]) + copy(values[actualStart+itemCount:], src.values[actualStart+actualSkipCount:]) + } + if itemCount > 0 { + copy(values[actualStart:], call.Arguments[2:]) + } + return r.newArrayValues(values) + } else { + a := r.newArrayLength(newLength) + var i int64 + rl := actualStart + actualSkipCount + + for i < actualStart { + pi := valueInt(i) + iValue := nilSafe(o.self.getIdx(pi, nil)) + createDataPropertyOrThrow(a, pi, iValue) + i++ + } + + if itemCount > 0 { + for _, item := range call.Arguments[2:] { + createDataPropertyOrThrow(a, valueInt(i), nilSafe(item)) + i++ + } + } + + for i < newLength { + pi := valueInt(i) + from := valueInt(rl) + fromValue := nilSafe(o.self.getIdx(from, nil)) + createDataPropertyOrThrow(a, pi, fromValue) + i++ + rl++ + } + + return a + } +} + +func (r *Runtime) checkStdArrayObj(obj *Object) *arrayObject { + if arr, ok := obj.self.(*arrayObject); ok && + arr.propValueCount == 0 && + arr.length == uint32(len(arr.values)) && + uint32(arr.objCount) == arr.length { + + return arr + } + + return nil +} + +func (r *Runtime) checkStdArrayObjWithProto(obj *Object) *arrayObject { + if arr := r.checkStdArrayObj(obj); arr != nil { + if p1, ok := arr.prototype.self.(*arrayObject); ok && p1.propValueCount == 0 { + if p2, ok := p1.prototype.self.(*baseObject); ok && p2.prototype == nil { + p2.ensurePropOrder() + if p2.idxPropCount == 0 { + return arr + } + } + } + } + return nil +} + +func (r *Runtime) checkStdArray(v Value) *arrayObject { + if obj, ok := v.(*Object); ok { + return r.checkStdArrayObj(obj) + } + + return nil +} + +func (r *Runtime) checkStdArrayIter(v Value) *arrayObject { + if arr := r.checkStdArray(v); arr != nil && + arr.getSym(SymIterator, nil) == r.getArrayValues() { + + return arr + } + + return nil +} + +func (r *Runtime) array_from(call FunctionCall) Value { + var mapFn func(FunctionCall) Value + if mapFnArg := call.Argument(1); mapFnArg != _undefined { + if mapFnObj, ok := mapFnArg.(*Object); ok { + if fn, ok := mapFnObj.self.assertCallable(); ok { + mapFn = fn + } + } + if mapFn == nil { + panic(r.NewTypeError("%s is not a function", mapFnArg)) + } + } + t := call.Argument(2) + items := call.Argument(0) + if mapFn == nil && call.This == r.global.Array { // mapFn may mutate the array + if arr := r.checkStdArrayIter(items); arr != nil { + items := make([]Value, len(arr.values)) + copy(items, arr.values) + return r.newArrayValues(items) + } + } + + var ctor func(args []Value, newTarget *Object) *Object + if call.This != r.global.Array { + if o, ok := call.This.(*Object); ok { + if c := o.self.assertConstructor(); c != nil { + ctor = c + } + } + } + var arr *Object + if usingIterator := toMethod(r.getV(items, SymIterator)); usingIterator != nil { + if ctor != nil { + arr = ctor([]Value{}, nil) + } else { + arr = r.newArrayValues(nil) + } + iter := r.getIterator(items, usingIterator) + if mapFn == nil { + if a := r.checkStdArrayObjWithProto(arr); a != nil { + var values []Value + iter.iterate(func(val Value) { + values = append(values, val) + }) + setArrayValues(a, values) + return arr + } + } + k := int64(0) + iter.iterate(func(val Value) { + if mapFn != nil { + val = mapFn(FunctionCall{This: t, Arguments: []Value{val, intToValue(k)}}) + } + createDataPropertyOrThrow(arr, intToValue(k), val) + k++ + }) + arr.self.setOwnStr("length", intToValue(k), true) + } else { + arrayLike := items.ToObject(r) + l := toLength(arrayLike.self.getStr("length", nil)) + if ctor != nil { + arr = ctor([]Value{intToValue(l)}, nil) + } else { + arr = r.newArrayValues(nil) + } + if mapFn == nil { + if a := r.checkStdArrayObjWithProto(arr); a != nil { + values := make([]Value, l) + for k := int64(0); k < l; k++ { + values[k] = nilSafe(arrayLike.self.getIdx(valueInt(k), nil)) + } + setArrayValues(a, values) + return arr + } + } + for k := int64(0); k < l; k++ { + idx := valueInt(k) + item := arrayLike.self.getIdx(idx, nil) + if mapFn != nil { + item = mapFn(FunctionCall{This: t, Arguments: []Value{item, idx}}) + } else { + item = nilSafe(item) + } + createDataPropertyOrThrow(arr, idx, item) + } + arr.self.setOwnStr("length", intToValue(l), true) + } + + return arr +} + +func (r *Runtime) array_isArray(call FunctionCall) Value { + if o, ok := call.Argument(0).(*Object); ok { + if isArray(o) { + return valueTrue + } + } + return valueFalse +} + +func (r *Runtime) array_of(call FunctionCall) Value { + var ctor func(args []Value, newTarget *Object) *Object + if call.This != r.global.Array { + if o, ok := call.This.(*Object); ok { + if c := o.self.assertConstructor(); c != nil { + ctor = c + } + } + } + if ctor == nil { + values := make([]Value, len(call.Arguments)) + copy(values, call.Arguments) + return r.newArrayValues(values) + } + l := intToValue(int64(len(call.Arguments))) + arr := ctor([]Value{l}, nil) + for i, val := range call.Arguments { + createDataPropertyOrThrow(arr, intToValue(int64(i)), val) + } + arr.self.setOwnStr("length", l, true) + return arr +} + +func (r *Runtime) arrayIterProto_next(call FunctionCall) Value { + thisObj := r.toObject(call.This) + if iter, ok := thisObj.self.(*arrayIterObject); ok { + return iter.next() + } + panic(r.NewTypeError("Method Array Iterator.prototype.next called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) +} + +func createArrayProtoTemplate() *objectTemplate { + t := newObjectTemplate() + t.protoFactory = func(r *Runtime) *Object { + return r.global.ObjectPrototype + } + + t.putStr("length", func(r *Runtime) Value { return valueProp(_positiveZero, true, false, false) }) + + t.putStr("constructor", func(r *Runtime) Value { return valueProp(r.getArray(), true, false, true) }) + + t.putStr("at", func(r *Runtime) Value { return r.methodProp(r.arrayproto_at, "at", 1) }) + t.putStr("concat", func(r *Runtime) Value { return r.methodProp(r.arrayproto_concat, "concat", 1) }) + t.putStr("copyWithin", func(r *Runtime) Value { return r.methodProp(r.arrayproto_copyWithin, "copyWithin", 2) }) + t.putStr("entries", func(r *Runtime) Value { return r.methodProp(r.arrayproto_entries, "entries", 0) }) + t.putStr("every", func(r *Runtime) Value { return r.methodProp(r.arrayproto_every, "every", 1) }) + t.putStr("fill", func(r *Runtime) Value { return r.methodProp(r.arrayproto_fill, "fill", 1) }) + t.putStr("filter", func(r *Runtime) Value { return r.methodProp(r.arrayproto_filter, "filter", 1) }) + t.putStr("find", func(r *Runtime) Value { return r.methodProp(r.arrayproto_find, "find", 1) }) + t.putStr("findIndex", func(r *Runtime) Value { return r.methodProp(r.arrayproto_findIndex, "findIndex", 1) }) + t.putStr("findLast", func(r *Runtime) Value { return r.methodProp(r.arrayproto_findLast, "findLast", 1) }) + t.putStr("findLastIndex", func(r *Runtime) Value { return r.methodProp(r.arrayproto_findLastIndex, "findLastIndex", 1) }) + t.putStr("flat", func(r *Runtime) Value { return r.methodProp(r.arrayproto_flat, "flat", 0) }) + t.putStr("flatMap", func(r *Runtime) Value { return r.methodProp(r.arrayproto_flatMap, "flatMap", 1) }) + t.putStr("forEach", func(r *Runtime) Value { return r.methodProp(r.arrayproto_forEach, "forEach", 1) }) + t.putStr("includes", func(r *Runtime) Value { return r.methodProp(r.arrayproto_includes, "includes", 1) }) + t.putStr("indexOf", func(r *Runtime) Value { return r.methodProp(r.arrayproto_indexOf, "indexOf", 1) }) + t.putStr("join", func(r *Runtime) Value { return r.methodProp(r.arrayproto_join, "join", 1) }) + t.putStr("keys", func(r *Runtime) Value { return r.methodProp(r.arrayproto_keys, "keys", 0) }) + t.putStr("lastIndexOf", func(r *Runtime) Value { return r.methodProp(r.arrayproto_lastIndexOf, "lastIndexOf", 1) }) + t.putStr("map", func(r *Runtime) Value { return r.methodProp(r.arrayproto_map, "map", 1) }) + t.putStr("pop", func(r *Runtime) Value { return r.methodProp(r.arrayproto_pop, "pop", 0) }) + t.putStr("push", func(r *Runtime) Value { return r.methodProp(r.arrayproto_push, "push", 1) }) + t.putStr("reduce", func(r *Runtime) Value { return r.methodProp(r.arrayproto_reduce, "reduce", 1) }) + t.putStr("reduceRight", func(r *Runtime) Value { return r.methodProp(r.arrayproto_reduceRight, "reduceRight", 1) }) + t.putStr("reverse", func(r *Runtime) Value { return r.methodProp(r.arrayproto_reverse, "reverse", 0) }) + t.putStr("shift", func(r *Runtime) Value { return r.methodProp(r.arrayproto_shift, "shift", 0) }) + t.putStr("slice", func(r *Runtime) Value { return r.methodProp(r.arrayproto_slice, "slice", 2) }) + t.putStr("some", func(r *Runtime) Value { return r.methodProp(r.arrayproto_some, "some", 1) }) + t.putStr("sort", func(r *Runtime) Value { return r.methodProp(r.arrayproto_sort, "sort", 1) }) + t.putStr("splice", func(r *Runtime) Value { return r.methodProp(r.arrayproto_splice, "splice", 2) }) + t.putStr("toLocaleString", func(r *Runtime) Value { return r.methodProp(r.arrayproto_toLocaleString, "toLocaleString", 0) }) + t.putStr("toString", func(r *Runtime) Value { return valueProp(r.getArrayToString(), true, false, true) }) + t.putStr("unshift", func(r *Runtime) Value { return r.methodProp(r.arrayproto_unshift, "unshift", 1) }) + t.putStr("with", func(r *Runtime) Value { return r.methodProp(r.arrayproto_with, "with", 2) }) + t.putStr("toReversed", func(r *Runtime) Value { return r.methodProp(r.arrayproto_toReversed, "toReversed", 0) }) + t.putStr("toSorted", func(r *Runtime) Value { return r.methodProp(r.arrayproto_toSorted, "toSorted", 1) }) + t.putStr("toSpliced", func(r *Runtime) Value { return r.methodProp(r.arrayproto_toSpliced, "toSpliced", 2) }) + t.putStr("values", func(r *Runtime) Value { return valueProp(r.getArrayValues(), true, false, true) }) + + t.putSym(SymIterator, func(r *Runtime) Value { return valueProp(r.getArrayValues(), true, false, true) }) + t.putSym(SymUnscopables, func(r *Runtime) Value { + bl := r.newBaseObject(nil, classObject) + bl.setOwnStr("copyWithin", valueTrue, true) + bl.setOwnStr("entries", valueTrue, true) + bl.setOwnStr("fill", valueTrue, true) + bl.setOwnStr("find", valueTrue, true) + bl.setOwnStr("findIndex", valueTrue, true) + bl.setOwnStr("findLast", valueTrue, true) + bl.setOwnStr("findLastIndex", valueTrue, true) + bl.setOwnStr("flat", valueTrue, true) + bl.setOwnStr("flatMap", valueTrue, true) + bl.setOwnStr("includes", valueTrue, true) + bl.setOwnStr("keys", valueTrue, true) + bl.setOwnStr("values", valueTrue, true) + bl.setOwnStr("groupBy", valueTrue, true) + bl.setOwnStr("groupByToMap", valueTrue, true) + bl.setOwnStr("toReversed", valueTrue, true) + bl.setOwnStr("toSorted", valueTrue, true) + bl.setOwnStr("toSpliced", valueTrue, true) + + return valueProp(bl.val, false, false, true) + }) + + return t +} + +var arrayProtoTemplate *objectTemplate +var arrayProtoTemplateOnce sync.Once + +func getArrayProtoTemplate() *objectTemplate { + arrayProtoTemplateOnce.Do(func() { + arrayProtoTemplate = createArrayProtoTemplate() + }) + return arrayProtoTemplate +} + +func (r *Runtime) getArrayPrototype() *Object { + ret := r.global.ArrayPrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.ArrayPrototype = ret + r.newTemplatedArrayObject(getArrayProtoTemplate(), ret) + } + return ret +} + +func (r *Runtime) getArray() *Object { + ret := r.global.Array + if ret == nil { + ret = &Object{runtime: r} + ret.self = r.createArray(ret) + r.global.Array = ret + } + return ret +} + +func (r *Runtime) createArray(val *Object) objectImpl { + o := r.newNativeFuncConstructObj(val, r.builtin_newArray, "Array", r.getArrayPrototype(), 1) + o._putProp("from", r.newNativeFunc(r.array_from, "from", 1), true, false, true) + o._putProp("isArray", r.newNativeFunc(r.array_isArray, "isArray", 1), true, false, true) + o._putProp("of", r.newNativeFunc(r.array_of, "of", 0), true, false, true) + r.putSpeciesReturnThis(o) + + return o +} + +func (r *Runtime) createArrayIterProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.getIteratorPrototype(), classObject) + + o._putProp("next", r.newNativeFunc(r.arrayIterProto_next, "next", 0), true, false, true) + o._putSym(SymToStringTag, valueProp(asciiString(classArrayIterator), false, false, true)) + + return o +} + +func (r *Runtime) getArrayValues() *Object { + ret := r.global.arrayValues + if ret == nil { + ret = r.newNativeFunc(r.arrayproto_values, "values", 0) + r.global.arrayValues = ret + } + return ret +} + +func (r *Runtime) getArrayToString() *Object { + ret := r.global.arrayToString + if ret == nil { + ret = r.newNativeFunc(r.arrayproto_toString, "toString", 0) + r.global.arrayToString = ret + } + return ret +} + +func (r *Runtime) getArrayIteratorPrototype() *Object { + var o *Object + if o = r.global.ArrayIteratorPrototype; o == nil { + o = &Object{runtime: r} + r.global.ArrayIteratorPrototype = o + o.self = r.createArrayIterProto(o) + } + return o + +} + +type sortable interface { + sortLen() int + sortGet(int) Value + swap(int, int) +} + +type arraySortCtx struct { + obj sortable + compare func(FunctionCall) Value +} + +func (a *arraySortCtx) sortCompare(x, y Value) int { + if x == nil && y == nil { + return 0 + } + + if x == nil { + return 1 + } + + if y == nil { + return -1 + } + + if x == _undefined && y == _undefined { + return 0 + } + + if x == _undefined { + return 1 + } + + if y == _undefined { + return -1 + } + + if a.compare != nil { + f := a.compare(FunctionCall{ + This: _undefined, + Arguments: []Value{x, y}, + }).ToFloat() + if f > 0 { + return 1 + } + if f < 0 { + return -1 + } + if math.Signbit(f) { + return -1 + } + return 0 + } + return x.toString().CompareTo(y.toString()) +} + +// sort.Interface + +func (a *arraySortCtx) Len() int { + return a.obj.sortLen() +} + +func (a *arraySortCtx) Less(j, k int) bool { + return a.sortCompare(a.obj.sortGet(j), a.obj.sortGet(k)) < 0 +} + +func (a *arraySortCtx) Swap(j, k int) { + a.obj.swap(j, k) +} diff --git a/goja/builtin_arrray_test.go b/goja/builtin_arrray_test.go new file mode 100644 index 0000000..2fcc15e --- /dev/null +++ b/goja/builtin_arrray_test.go @@ -0,0 +1,367 @@ +package goja + +import ( + "testing" +) + +func TestArrayProtoProp(t *testing.T) { + const SCRIPT = ` + Object.defineProperty(Array.prototype, '0', {value: 42, configurable: true, writable: false}) + var a = [] + a[0] = 1 + a[0] + ` + + testScript(SCRIPT, valueInt(42), t) +} + +func TestArrayDelete(t *testing.T) { + const SCRIPT = ` + var a = [1, 2]; + var deleted = delete a[0]; + var undef = a[0] === undefined; + var len = a.length; + + deleted && undef && len === 2; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestArrayDeleteNonexisting(t *testing.T) { + const SCRIPT = ` + Array.prototype[0] = 42; + var a = []; + delete a[0] && a[0] === 42; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestArraySetLength(t *testing.T) { + const SCRIPT = ` + var a = [1, 2]; + var assert0 = a.length == 2; + a.length = "1"; + a.length = 1.0; + a.length = 1; + var assert1 = a.length == 1; + a.length = 2; + var assert2 = a.length == 2; + assert0 && assert1 && assert2 && a[1] === undefined; + + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestArrayReverseNonOptimisable(t *testing.T) { + const SCRIPT = ` + var a = []; + Object.defineProperty(a, "0", {get: function() {return 42}, set: function(v) {Object.defineProperty(a, "0", {value: v + 1, writable: true, configurable: true})}, configurable: true}) + a[1] = 43; + a.reverse(); + + a.length === 2 && a[0] === 44 && a[1] === 42; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestArrayPushNonOptimisable(t *testing.T) { + const SCRIPT = ` + Object.defineProperty(Object.prototype, "0", {value: 42}); + var a = []; + var thrown = false; + try { + a.push(1); + } catch (e) { + thrown = e instanceof TypeError; + } + thrown; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestArraySetLengthWithPropItems(t *testing.T) { + const SCRIPT = ` + var a = [1,2,3,4]; + var thrown = false; + + Object.defineProperty(a, "2", {value: 42, configurable: false, writable: false}); + try { + Object.defineProperty(a, "length", {value: 0, writable: false}); + } catch (e) { + thrown = e instanceof TypeError; + } + thrown && a.length === 3; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestArrayFrom(t *testing.T) { + const SCRIPT = ` + function checkDestHoles(dest, prefix) { + assert(dest !== source, prefix + ": dest !== source"); + assert.sameValue(dest.length, 3, prefix + ": dest.length"); + assert.sameValue(dest[0], 1, prefix + ": [0]"); + assert.sameValue(dest[1], undefined, prefix + ": [1]"); + assert(dest.hasOwnProperty("1"), prefix + ': hasOwnProperty("1")'); + assert.sameValue(dest[2], 3, prefix + ": [2]"); + } + + function checkDest(dest, prefix) { + assert(dest !== source, prefix + ": dest !== source"); + assert.sameValue(dest.length, 3, prefix + ": dest.length"); + assert.sameValue(dest[0], 1, prefix + ": [0]"); + assert.sameValue(dest[1], 2, prefix + ": [1]"); + assert.sameValue(dest[2], 3, prefix + ": [2]"); + } + + var source = [1,2,3]; + var srcHoles = [1,,3]; + + checkDest(Array.from(source), "std source/std dest"); + checkDestHoles(Array.from(srcHoles), "std source (holes)/std dest"); + + function Iter() { + this.idx = 0; + } + Iter.prototype.next = function() { + if (this.idx < source.length) { + return {value: source[this.idx++]}; + } else { + return {done: true}; + } + } + + var src = {}; + src[Symbol.iterator] = function() { + return new Iter(); + } + checkDest(Array.from(src), "iter src/std dest"); + + src = {0: 1, 2: 3, length: 3}; + checkDestHoles(Array.from(src), "arrayLike src/std dest"); + + function A() {} + A.from = Array.from; + + checkDest(A.from(source), "std src/cust dest"); + checkDestHoles(A.from(srcHoles), "std src (holes)/cust dest"); + checkDestHoles(A.from(src), "arrayLike src/cust dest"); + + function T2() { + Object.defineProperty(this, 0, { + configurable: false, + writable: true, + enumerable: true + }); + } + + assert.throws(TypeError, function() { + Array.from.call(T2, source); + }); + + ` + + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestArrayOf(t *testing.T) { + const SCRIPT = ` + function T1() { + Object.preventExtensions(this); + } + + assert.throws(TypeError, function() { + Array.of.call(T1, 'Bob'); + }); + + function T2() { + Object.defineProperty(this, 0, { + configurable: false, + writable: true, + enumerable: true + }); + } + + assert.throws(TypeError, function() { + Array.of.call(T2, 'Bob'); + }) + + result = Array.of.call(undefined); + assert( + result instanceof Array, + 'this is not a constructor' + ); + + result = Array.of.call(Math.cos); + assert( + result instanceof Array, + 'this is a builtin function with no [[Construct]] slot' + ); + + ` + + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestUnscopables(t *testing.T) { + const SCRIPT = ` + var keys = []; + var _length; + with (Array.prototype) { + _length = length; + keys.push('something'); + } + _length === 0 && keys.length === 1 && keys[0] === "something"; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestArraySort(t *testing.T) { + const SCRIPT = ` + assert.throws(TypeError, function() { + [1,2].sort(null); + }, "null compare function"); + assert.throws(TypeError, function() { + [1,2].sort({}); + }, "non-callable compare function"); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestArraySortNonStdArray(t *testing.T) { + const SCRIPT = ` + const array = [undefined, 'c', /*hole*/, 'b', undefined, /*hole*/, 'a', 'd']; + + Object.defineProperty(array, '2', { + get() { + array.pop(); + array.pop(); + return this.foo; + }, + set(v) { + this.foo = v; + } + }); + + array.sort(); + + assert.sameValue(array[0], 'b'); + assert.sameValue(array[1], 'c'); + assert.sameValue(array[3], undefined); + assert.sameValue(array[4], undefined); + assert.sameValue('5' in array, false); + assert.sameValue(array.hasOwnProperty('5'), false); + assert.sameValue(array.length, 6); + assert.sameValue(array.foo, undefined); + + assert.sameValue(array[2], undefined); + assert.sameValue(array.length, 4); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestArrayConcat(t *testing.T) { + const SCRIPT = ` + var concat = Array.prototype.concat; + var array = [1, 2]; + var sparseArray = [1, , 2]; + var nonSpreadableArray = [1, 2]; + nonSpreadableArray[Symbol.isConcatSpreadable] = false; + var arrayLike = { 0: 1, 1: 2, length: 2 }; + var spreadableArrayLike = { 0: 1, 1: 2, length: 2 }; + spreadableArrayLike[Symbol.isConcatSpreadable] = true; + assert(looksNative(concat)); + assert(deepEqual(array.concat(), [1, 2]), '#1'); + assert(deepEqual(sparseArray.concat(), [1, , 2]), '#2'); + assert(deepEqual(nonSpreadableArray.concat(), [[1, 2]]), '#3'); + assert(deepEqual(concat.call(arrayLike), [{ 0: 1, 1: 2, length: 2 }]), '#4'); + assert(deepEqual(concat.call(spreadableArrayLike), [1, 2]), '#5'); + assert(deepEqual([].concat(array), [1, 2]), '#6'); + assert(deepEqual([].concat(sparseArray), [1, , 2]), '#7'); + assert(deepEqual([].concat(nonSpreadableArray), [[1, 2]]), '#8'); + assert(deepEqual([].concat(arrayLike), [{ 0: 1, 1: 2, length: 2 }]), '#9'); + assert(deepEqual([].concat(spreadableArrayLike), [1, 2]), '#10'); + assert(deepEqual(array.concat(sparseArray, nonSpreadableArray, arrayLike, spreadableArrayLike), [ + 1, 2, 1, , 2, [1, 2], { 0: 1, 1: 2, length: 2 }, 1, 2, + ]), '#11'); + array = []; + array.constructor = {}; + array.constructor[Symbol.species] = function () { + return { foo: 1 }; + } + assert.sameValue(array.concat().foo, 1, '@@species'); + ` + testScriptWithTestLibX(SCRIPT, _undefined, t) +} + +func TestArrayFlat(t *testing.T) { + const SCRIPT = ` + var array = [1, [2,3,[4,5,6]], [[[[7,8,9]]]]]; + assert(deepEqual(array.flat(), [1,2,3,[4,5,6],[[[7,8,9]]]]), '#1'); + assert(deepEqual(array.flat(1), [1,2,3,[4,5,6],[[[7,8,9]]]]), '#2'); + assert(deepEqual(array.flat(3), [1,2,3,4,5,6,[7,8,9]]), '#3'); + assert(deepEqual(array.flat(4), [1,2,3,4,5,6,7,8,9]), '#4'); + assert(deepEqual(array.flat(10), [1,2,3,4,5,6,7,8,9]), '#5'); + ` + testScriptWithTestLibX(SCRIPT, _undefined, t) +} + +func TestArrayFlatMap(t *testing.T) { + const SCRIPT = ` + var double = function(x) { + if (isNaN(x)) { + return x + } + return x * 2 + } + var array = [1, [2,3,[4,5,6]], [[[[7,8,9]]]]]; + assert(deepEqual(array.flatMap(double), [2,2,3,[4,5,6],[[[7,8,9]]]]), '#1'); + ` + testScriptWithTestLibX(SCRIPT, _undefined, t) +} + +func TestArrayProto(t *testing.T) { + const SCRIPT = ` + const a = Array.prototype; + a.push(1, 2, 3, 4, 5); + assert.sameValue(a.length, 5); + assert.sameValue(a[0], 1); + a.length = 3; + assert.sameValue(a.length, 3); + assert(compareArray(a, [1, 2, 3])); + a.shift(); + assert.sameValue(a.length, 2); + assert(compareArray(a, [2, 3])); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestArrayToSpliced(t *testing.T) { + const SCRIPT = ` + const a = [1, 2, 3]; + a.push(4) + assert(compareArray(a, [1, 2, 3, 4])); + const b = a.toSpliced(2) + assert(compareArray(a, [1, 2, 3, 4])); + assert(compareArray(b, [1, 2])); + a.push(5) + const c = a.toSpliced(1, 2); + assert(compareArray(a, [1, 2, 3, 4, 5])); + assert(compareArray(c, [1, 4, 5])); + assert(compareArray(a.toSpliced(4, 2, 'a', 'b'), [1, 2, 3, 4, 'a', 'b'])); + assert(compareArray(a, [1, 2, 3, 4, 5])); + assert(compareArray(a.toSpliced(-2, 2), [1, 2, 3])); + assert(compareArray(a, [1, 2, 3, 4, 5])); + assert(compareArray(a.toSpliced(2, 10), [1, 2])); + assert(compareArray(a, [1, 2, 3, 4, 5])); + assert(compareArray(a.toSpliced(1, 0, 'a'), [1, 'a', 2, 3, 4, 5])); + assert(compareArray(a, [1, 2, 3, 4, 5])); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} diff --git a/goja/builtin_bigint.go b/goja/builtin_bigint.go new file mode 100644 index 0000000..a50ebcf --- /dev/null +++ b/goja/builtin_bigint.go @@ -0,0 +1,369 @@ +package goja + +import ( + "fmt" + "hash/maphash" + "math" + "math/big" + "reflect" + "strconv" + "sync" + + "github.com/dop251/goja/unistring" +) + +type valueBigInt big.Int + +func (v *valueBigInt) ToInteger() int64 { + v.ToNumber() + return 0 +} + +func (v *valueBigInt) toString() String { + return asciiString((*big.Int)(v).String()) +} + +func (v *valueBigInt) string() unistring.String { + return unistring.String(v.String()) +} + +func (v *valueBigInt) ToString() Value { + return v +} + +func (v *valueBigInt) String() string { + return (*big.Int)(v).String() +} + +func (v *valueBigInt) ToFloat() float64 { + v.ToNumber() + return 0 +} + +func (v *valueBigInt) ToNumber() Value { + panic(typeError("Cannot convert a BigInt value to a number")) +} + +func (v *valueBigInt) ToBoolean() bool { + return (*big.Int)(v).Sign() != 0 +} + +func (v *valueBigInt) ToObject(r *Runtime) *Object { + return r.newPrimitiveObject(v, r.getBigIntPrototype(), classObject) +} + +func (v *valueBigInt) SameAs(other Value) bool { + if o, ok := other.(*valueBigInt); ok { + return (*big.Int)(v).Cmp((*big.Int)(o)) == 0 + } + return false +} + +func (v *valueBigInt) Equals(other Value) bool { + switch o := other.(type) { + case *valueBigInt: + return (*big.Int)(v).Cmp((*big.Int)(o)) == 0 + case valueInt: + return (*big.Int)(v).Cmp(big.NewInt(int64(o))) == 0 + case valueFloat: + if IsInfinity(o) || math.IsNaN(float64(o)) { + return false + } + if f := big.NewFloat(float64(o)); f.IsInt() { + i, _ := f.Int(nil) + return (*big.Int)(v).Cmp(i) == 0 + } + return false + case String: + bigInt, err := stringToBigInt(o.toTrimmedUTF8()) + if err != nil { + return false + } + return bigInt.Cmp((*big.Int)(v)) == 0 + case valueBool: + return (*big.Int)(v).Int64() == o.ToInteger() + case *Object: + return v.Equals(o.toPrimitiveNumber()) + } + return false +} + +func (v *valueBigInt) StrictEquals(other Value) bool { + o, ok := other.(*valueBigInt) + if ok { + return (*big.Int)(v).Cmp((*big.Int)(o)) == 0 + } + return false +} + +func (v *valueBigInt) Export() interface{} { + return new(big.Int).Set((*big.Int)(v)) +} + +func (v *valueBigInt) ExportType() reflect.Type { + return typeBigInt +} + +func (v *valueBigInt) baseObject(rt *Runtime) *Object { + return rt.getBigIntPrototype() +} + +func (v *valueBigInt) hash(hash *maphash.Hash) uint64 { + var sign byte + if (*big.Int)(v).Sign() < 0 { + sign = 0x01 + } else { + sign = 0x00 + } + _ = hash.WriteByte(sign) + _, _ = hash.Write((*big.Int)(v).Bytes()) + h := hash.Sum64() + hash.Reset() + return h +} + +func toBigInt(value Value) *valueBigInt { + // Undefined Throw a TypeError exception. + // Null Throw a TypeError exception. + // Boolean Return 1n if prim is true and 0n if prim is false. + // BigInt Return prim. + // Number Throw a TypeError exception. + // String 1. Let n be StringToBigInt(prim). + // 2. If n is undefined, throw a SyntaxError exception. + // 3. Return n. + // Symbol Throw a TypeError exception. + switch prim := value.(type) { + case *valueBigInt: + return prim + case String: + bigInt, err := stringToBigInt(prim.toTrimmedUTF8()) + if err != nil { + panic(syntaxError(fmt.Sprintf("Cannot convert %s to a BigInt", prim))) + } + return (*valueBigInt)(bigInt) + case valueBool: + return (*valueBigInt)(big.NewInt(prim.ToInteger())) + case *Symbol: + panic(typeError("Cannot convert Symbol to a BigInt")) + case *Object: + return toBigInt(prim.toPrimitiveNumber()) + default: + panic(typeError(fmt.Sprintf("Cannot convert %s to a BigInt", prim))) + } +} + +func numberToBigInt(v Value) *valueBigInt { + switch v := toNumeric(v).(type) { + case *valueBigInt: + return v + case valueInt: + return (*valueBigInt)(big.NewInt(v.ToInteger())) + case valueFloat: + if IsInfinity(v) || math.IsNaN(float64(v)) { + panic(rangeError(fmt.Sprintf("Cannot convert %s to a BigInt", v))) + } + if f := big.NewFloat(float64(v)); f.IsInt() { + n, _ := f.Int(nil) + return (*valueBigInt)(n) + } + panic(rangeError(fmt.Sprintf("Cannot convert %s to a BigInt", v))) + case *Object: + prim := v.toPrimitiveNumber() + switch prim.(type) { + case valueInt, valueFloat: + return numberToBigInt(prim) + default: + return toBigInt(prim) + } + default: + panic(newTypeError("Cannot convert %s to a BigInt", v)) + } +} + +func stringToBigInt(str string) (*big.Int, error) { + var bigint big.Int + n, err := stringToInt(str) + if err != nil { + switch { + case isRangeErr(err): + bigint.SetString(str, 0) + case err == strconv.ErrSyntax: + default: + return nil, strconv.ErrSyntax + } + } else { + bigint.SetInt64(n) + } + return &bigint, nil +} + +func (r *Runtime) thisBigIntValue(value Value) Value { + switch t := value.(type) { + case *valueBigInt: + return t + case *Object: + switch t := t.self.(type) { + case *primitiveValueObject: + return r.thisBigIntValue(t.pValue) + case *objectGoReflect: + if t.exportType() == typeBigInt && t.valueOf != nil { + return t.valueOf() + } + } + } + panic(r.NewTypeError("requires that 'this' be a BigInt")) +} + +func (r *Runtime) bigintproto_valueOf(call FunctionCall) Value { + return r.thisBigIntValue(call.This) +} + +func (r *Runtime) bigintproto_toString(call FunctionCall) Value { + x := (*big.Int)(r.thisBigIntValue(call.This).(*valueBigInt)) + radix := call.Argument(0) + var radixMV int + + if radix == _undefined { + radixMV = 10 + } else { + radixMV = int(radix.ToInteger()) + if radixMV < 2 || radixMV > 36 { + panic(r.newError(r.getRangeError(), "radix must be an integer between 2 and 36")) + } + } + + return asciiString(x.Text(radixMV)) +} + +func (r *Runtime) bigint_asIntN(call FunctionCall) Value { + if len(call.Arguments) < 2 { + panic(r.NewTypeError("Cannot convert undefined to a BigInt")) + } + bits := r.toIndex(call.Argument(0).ToNumber()) + if bits < 0 { + panic(r.NewTypeError("Invalid value: not (convertible to) a safe integer")) + } + bigint := toBigInt(call.Argument(1)) + + twoToBits := new(big.Int).Lsh(big.NewInt(1), uint(bits)) + mod := new(big.Int).Mod((*big.Int)(bigint), twoToBits) + if bits > 0 && mod.Cmp(new(big.Int).Lsh(big.NewInt(1), uint(bits-1))) >= 0 { + return (*valueBigInt)(mod.Sub(mod, twoToBits)) + } else { + return (*valueBigInt)(mod) + } +} + +func (r *Runtime) bigint_asUintN(call FunctionCall) Value { + if len(call.Arguments) < 2 { + panic(r.NewTypeError("Cannot convert undefined to a BigInt")) + } + bits := r.toIndex(call.Argument(0).ToNumber()) + if bits < 0 { + panic(r.NewTypeError("Invalid value: not (convertible to) a safe integer")) + } + bigint := (*big.Int)(toBigInt(call.Argument(1))) + ret := new(big.Int).Mod(bigint, new(big.Int).Lsh(big.NewInt(1), uint(bits))) + return (*valueBigInt)(ret) +} + +var bigintTemplate *objectTemplate +var bigintTemplateOnce sync.Once + +func getBigIntTemplate() *objectTemplate { + bigintTemplateOnce.Do(func() { + bigintTemplate = createBigIntTemplate() + }) + return bigintTemplate +} + +func createBigIntTemplate() *objectTemplate { + t := newObjectTemplate() + t.protoFactory = func(r *Runtime) *Object { + return r.getFunctionPrototype() + } + + t.putStr("name", func(r *Runtime) Value { return valueProp(asciiString("BigInt"), false, false, true) }) + t.putStr("length", func(r *Runtime) Value { return valueProp(intToValue(1), false, false, true) }) + t.putStr("prototype", func(r *Runtime) Value { return valueProp(r.getBigIntPrototype(), false, false, false) }) + + t.putStr("asIntN", func(r *Runtime) Value { return r.methodProp(r.bigint_asIntN, "asIntN", 2) }) + t.putStr("asUintN", func(r *Runtime) Value { return r.methodProp(r.bigint_asUintN, "asUintN", 2) }) + + return t +} + +func (r *Runtime) builtin_BigInt(call FunctionCall) Value { + if len(call.Arguments) > 0 { + switch v := call.Argument(0).(type) { + case *valueBigInt, valueInt, valueFloat, *Object: + return numberToBigInt(v) + default: + return toBigInt(v) + } + } + return (*valueBigInt)(big.NewInt(0)) +} + +func (r *Runtime) builtin_newBigInt(args []Value, newTarget *Object) *Object { + if newTarget != nil { + panic(r.NewTypeError("BigInt is not a constructor")) + } + var v Value + if len(args) > 0 { + v = numberToBigInt(args[0]) + } else { + v = (*valueBigInt)(big.NewInt(0)) + } + return r.newPrimitiveObject(v, newTarget, classObject) +} + +func (r *Runtime) getBigInt() *Object { + ret := r.global.BigInt + if ret == nil { + ret = &Object{runtime: r} + r.global.BigInt = ret + r.newTemplatedFuncObject(getBigIntTemplate(), ret, r.builtin_BigInt, + r.wrapNativeConstruct(r.builtin_newBigInt, ret, r.getBigIntPrototype())) + } + return ret +} + +func createBigIntProtoTemplate() *objectTemplate { + t := newObjectTemplate() + t.protoFactory = func(r *Runtime) *Object { + return r.global.ObjectPrototype + } + + t.putStr("length", func(r *Runtime) Value { return valueProp(intToValue(0), false, false, true) }) + t.putStr("name", func(r *Runtime) Value { return valueProp(asciiString("BigInt"), false, false, true) }) + t.putStr("constructor", func(r *Runtime) Value { return valueProp(r.getBigInt(), true, false, true) }) + + t.putStr("toLocaleString", func(r *Runtime) Value { return r.methodProp(r.bigintproto_toString, "toLocaleString", 0) }) + t.putStr("toString", func(r *Runtime) Value { return r.methodProp(r.bigintproto_toString, "toString", 0) }) + t.putStr("valueOf", func(r *Runtime) Value { return r.methodProp(r.bigintproto_valueOf, "valueOf", 0) }) + t.putSym(SymToStringTag, func(r *Runtime) Value { return valueProp(asciiString("BigInt"), false, false, true) }) + + return t +} + +var bigintProtoTemplate *objectTemplate +var bigintProtoTemplateOnce sync.Once + +func getBigIntProtoTemplate() *objectTemplate { + bigintProtoTemplateOnce.Do(func() { + bigintProtoTemplate = createBigIntProtoTemplate() + }) + return bigintProtoTemplate +} + +func (r *Runtime) getBigIntPrototype() *Object { + ret := r.global.BigIntPrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.BigIntPrototype = ret + o := r.newTemplatedObject(getBigIntProtoTemplate(), ret) + o.class = classObject + } + return ret +} diff --git a/goja/builtin_bigint_test.go b/goja/builtin_bigint_test.go new file mode 100644 index 0000000..79df861 --- /dev/null +++ b/goja/builtin_bigint_test.go @@ -0,0 +1,117 @@ +package goja + +import ( + "math/big" + "testing" +) + +func TestBigInt(t *testing.T) { + const SCRIPT = `0xabcdef0123456789abcdef0123n` + b := new(big.Int) + b.SetString("0xabcdef0123456789abcdef0123", 0) + testScript(SCRIPT, (*valueBigInt)(b), t) +} + +func TestBigIntExportTo(t *testing.T) { + vm := New() + + t.Run("bigint exportType", func(t *testing.T) { + v, err := vm.RunString(`BigInt(Number.MAX_SAFE_INTEGER + 10);`) + if err != nil { + t.Fatal(err) + } + if typ := v.ExportType(); typ != typeBigInt { + t.Fatal(typ) + } + }) + + t.Run("bigint", func(t *testing.T) { + var b big.Int + err := vm.ExportTo(vm.ToValue(big.NewInt(10)), &b) + if err != nil { + t.Fatal(err) + } + if b.Cmp(big.NewInt(10)) != 0 { + t.Fatalf("bigint: %s", b.String()) + } + }) +} + +func TestBigIntFormat(t *testing.T) { + const SCRIPT = ` +assert.sameValue((1n).toString(undefined), "1", "radius undefined"); +assert.throws(RangeError, () => { (1n).toString(-1); }, "radius -1"); +assert.throws(RangeError, () => { (1n).toString(37); }, "radius 37"); +assert.sameValue((1n).toString(2), "1", "radius 2"); +assert.sameValue((10n).toString(3), "101", "radius 3"); +` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestBigIntOperator(t *testing.T) { + const SCRIPT = ` +assert.throws(TypeError, () => { 1 - 1n; }, "mix type add"); +assert.throws(TypeError, () => { 1n - 1; }, "mix type add"); +assert.throws(TypeError, () => { 1n + 1; }, "mix type sub"); +assert.throws(TypeError, () => { 1 + 1n; }, "mix type sub"); +assert.throws(TypeError, () => { 1 * 1n; }, "mix type mul"); +assert.throws(TypeError, () => { 1n * 1; }, "mix type mul"); +assert.throws(TypeError, () => { 1 / 1n; }, "mix type div"); +assert.throws(TypeError, () => { 1n / 1; }, "mix type div"); +assert.throws(TypeError, () => { 1 % 1n; }, "mix type mod"); +assert.throws(TypeError, () => { 1n % 1; }, "mix type mod"); +assert.throws(TypeError, () => { 1n ** 1; }, "mix type exp"); +assert.throws(TypeError, () => { 1 ** 1n; }, "mix type exp"); +assert.throws(TypeError, () => { 1 & 1n; }, "mix type and"); +assert.throws(TypeError, () => { 1n & 1; }, "mix type and"); +assert.throws(TypeError, () => { 1 | 1n; }, "mix type or"); +assert.throws(TypeError, () => { 1n | 1; }, "mix type or"); +assert.throws(TypeError, () => { 1 ^ 1n; }, "mix type xor"); +assert.throws(TypeError, () => { 1n ^ 1; }, "mix type xor"); +assert.throws(TypeError, () => { 1 << 1n; }, "mix type lsh"); +assert.throws(TypeError, () => { 1n << 1; }, "mix type lsh"); +assert.throws(TypeError, () => { 1 >> 1n; }, "mix type rsh"); +assert.throws(TypeError, () => { 1n >> 1; }, "mix type rsh"); +assert.throws(TypeError, () => { 1 >>> 1n; }, "mix type ursh"); +assert.throws(TypeError, () => { 1n >>> 1; }, "mix type ursh"); + +assert.sameValue(1n + 1n, 2n, "add"); +assert.sameValue(1n - 1n, 0n, "sub"); +assert.sameValue(1n * 2n, 2n, "mul"); +assert.sameValue(1n / 2n, 0n, "div"); +assert.sameValue(1n % 2n, 1n, "mod"); +assert.sameValue(1n ** 2n, 1n, "exp"); +assert.sameValue(1n & 1n, 1n, "and"); +assert.sameValue(1n | 1n, 1n, "or"); +assert.sameValue(2n ^ 1n, 3n, "xor"); +assert.sameValue(1n << 1n, 2n, "lsh"); +assert.sameValue(4n << -1n, 2n, "neg lsh"); +assert.sameValue(4n >> 1n, 2n, "rsh"); +assert.sameValue(2n >> -2n, 8n, "neg rsh"); + +let a = 1n; +assert.sameValue(++a, 2n, "inc"); +assert.sameValue(--a, 1n, "dec"); + +assert.sameValue(Object(1n) - 1n, 0n, "primitive sub"); +assert.sameValue(Object(Object(1n)) - 1n, 0n, "primitive sub"); +assert.sameValue({ [Symbol.toPrimitive]: () => 1n } - 1n, 0n, "primitive sub"); +assert.sameValue({ valueOf: () => 1n } - 1n, 0n, "valueOf sub"); + +assert.sameValue(1n > 0, true, "gt"); +assert.sameValue(0 > 1n, false, "gt"); +assert.sameValue(Object(1n) > 0, true, "gt"); +assert.sameValue(0 > Object(1n), false, "gt"); + +assert.sameValue(1n < 0, false, "lt"); +assert.sameValue(0 < 1n, true, "lt"); +assert.sameValue(Object(1n) < 0, false, "lt"); +assert.sameValue(0 < Object(1n), true, "lt"); + +assert.sameValue(1n >= 0, true, "ge"); +assert.sameValue(0 >= 1n, false, "ge"); +assert.sameValue(1n <= 0, false, "le"); +assert.sameValue(0 <= 1n, true, "le"); +` + testScriptWithTestLib(SCRIPT, _undefined, t) +} diff --git a/goja/builtin_boolean.go b/goja/builtin_boolean.go new file mode 100644 index 0000000..8476328 --- /dev/null +++ b/goja/builtin_boolean.go @@ -0,0 +1,75 @@ +package goja + +func (r *Runtime) booleanproto_toString(call FunctionCall) Value { + var b bool + switch o := call.This.(type) { + case valueBool: + b = bool(o) + goto success + case *Object: + if p, ok := o.self.(*primitiveValueObject); ok { + if b1, ok := p.pValue.(valueBool); ok { + b = bool(b1) + goto success + } + } + if o, ok := o.self.(*objectGoReflect); ok { + if o.class == classBoolean && o.toString != nil { + return o.toString() + } + } + } + r.typeErrorResult(true, "Method Boolean.prototype.toString is called on incompatible receiver") + +success: + if b { + return stringTrue + } + return stringFalse +} + +func (r *Runtime) booleanproto_valueOf(call FunctionCall) Value { + switch o := call.This.(type) { + case valueBool: + return o + case *Object: + if p, ok := o.self.(*primitiveValueObject); ok { + if b, ok := p.pValue.(valueBool); ok { + return b + } + } + if o, ok := o.self.(*objectGoReflect); ok { + if o.class == classBoolean && o.valueOf != nil { + return o.valueOf() + } + } + } + + r.typeErrorResult(true, "Method Boolean.prototype.valueOf is called on incompatible receiver") + return nil +} + +func (r *Runtime) getBooleanPrototype() *Object { + ret := r.global.BooleanPrototype + if ret == nil { + ret = r.newPrimitiveObject(valueFalse, r.global.ObjectPrototype, classBoolean) + r.global.BooleanPrototype = ret + o := ret.self + o._putProp("toString", r.newNativeFunc(r.booleanproto_toString, "toString", 0), true, false, true) + o._putProp("valueOf", r.newNativeFunc(r.booleanproto_valueOf, "valueOf", 0), true, false, true) + o._putProp("constructor", r.getBoolean(), true, false, true) + } + return ret +} + +func (r *Runtime) getBoolean() *Object { + ret := r.global.Boolean + if ret == nil { + ret = &Object{runtime: r} + r.global.Boolean = ret + proto := r.getBooleanPrototype() + r.newNativeFuncAndConstruct(ret, r.builtin_Boolean, + r.wrapNativeConstruct(r.builtin_newBoolean, ret, proto), proto, "Boolean", intToValue(1)) + } + return ret +} diff --git a/goja/builtin_date.go b/goja/builtin_date.go new file mode 100644 index 0000000..84a80ac --- /dev/null +++ b/goja/builtin_date.go @@ -0,0 +1,1058 @@ +package goja + +import ( + "fmt" + "math" + "sync" + "time" +) + +func (r *Runtime) makeDate(args []Value, utc bool) (t time.Time, valid bool) { + switch { + case len(args) >= 2: + t = time.Date(1970, time.January, 1, 0, 0, 0, 0, time.Local) + t, valid = _dateSetYear(t, FunctionCall{Arguments: args}, 0, utc) + case len(args) == 0: + t = r.now() + valid = true + default: // one argument + if o, ok := args[0].(*Object); ok { + if d, ok := o.self.(*dateObject); ok { + t = d.time() + valid = true + } + } + if !valid { + pv := toPrimitive(args[0]) + if val, ok := pv.(String); ok { + return dateParse(val.String()) + } + pv = pv.ToNumber() + var n int64 + if i, ok := pv.(valueInt); ok { + n = int64(i) + } else if f, ok := pv.(valueFloat); ok { + f := float64(f) + if math.IsNaN(f) || math.IsInf(f, 0) { + return + } + if math.Abs(f) > maxTime { + return + } + n = int64(f) + } else { + n = pv.ToInteger() + } + t = timeFromMsec(n) + valid = true + } + } + if valid { + msec := t.Unix()*1000 + int64(t.Nanosecond()/1e6) + if msec < 0 { + msec = -msec + } + if msec > maxTime { + valid = false + } + } + return +} + +func (r *Runtime) newDateTime(args []Value, proto *Object) *Object { + t, isSet := r.makeDate(args, false) + return r.newDateObject(t, isSet, proto) +} + +func (r *Runtime) builtin_newDate(args []Value, proto *Object) *Object { + return r.newDateTime(args, proto) +} + +func (r *Runtime) builtin_date(FunctionCall) Value { + return asciiString(dateFormat(r.now())) +} + +func (r *Runtime) date_parse(call FunctionCall) Value { + t, set := dateParse(call.Argument(0).toString().String()) + if set { + return intToValue(timeToMsec(t)) + } + return _NaN +} + +func (r *Runtime) date_UTC(call FunctionCall) Value { + var args []Value + if len(call.Arguments) < 2 { + args = []Value{call.Argument(0), _positiveZero} + } else { + args = call.Arguments + } + t, valid := r.makeDate(args, true) + if !valid { + return _NaN + } + return intToValue(timeToMsec(t)) +} + +func (r *Runtime) date_now(FunctionCall) Value { + return intToValue(timeToMsec(r.now())) +} + +func (r *Runtime) dateproto_toString(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return asciiString(d.time().Format(dateTimeLayout)) + } else { + return stringInvalidDate + } + } + panic(r.NewTypeError("Method Date.prototype.toString is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_toUTCString(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return asciiString(d.timeUTC().Format(utcDateTimeLayout)) + } else { + return stringInvalidDate + } + } + panic(r.NewTypeError("Method Date.prototype.toUTCString is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_toISOString(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + utc := d.timeUTC() + year := utc.Year() + if year >= -9999 && year <= 9999 { + return asciiString(utc.Format(isoDateTimeLayout)) + } + // extended year + return asciiString(fmt.Sprintf("%+06d-", year) + utc.Format(isoDateTimeLayout[5:])) + } else { + panic(r.newError(r.getRangeError(), "Invalid time value")) + } + } + panic(r.NewTypeError("Method Date.prototype.toISOString is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_toJSON(call FunctionCall) Value { + obj := call.This.ToObject(r) + tv := obj.toPrimitiveNumber() + if f, ok := tv.(valueFloat); ok { + f := float64(f) + if math.IsNaN(f) || math.IsInf(f, 0) { + return _null + } + } + + if toISO, ok := obj.self.getStr("toISOString", nil).(*Object); ok { + if toISO, ok := toISO.self.assertCallable(); ok { + return toISO(FunctionCall{ + This: obj, + }) + } + } + + panic(r.NewTypeError("toISOString is not a function")) +} + +func (r *Runtime) dateproto_toPrimitive(call FunctionCall) Value { + o := r.toObject(call.This) + arg := call.Argument(0) + + if asciiString("string").StrictEquals(arg) || asciiString("default").StrictEquals(arg) { + return o.ordinaryToPrimitiveString() + } + if asciiString("number").StrictEquals(arg) { + return o.ordinaryToPrimitiveNumber() + } + panic(r.NewTypeError("Invalid hint: %s", arg)) +} + +func (r *Runtime) dateproto_toDateString(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return asciiString(d.time().Format(dateLayout)) + } else { + return stringInvalidDate + } + } + panic(r.NewTypeError("Method Date.prototype.toDateString is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_toTimeString(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return asciiString(d.time().Format(timeLayout)) + } else { + return stringInvalidDate + } + } + panic(r.NewTypeError("Method Date.prototype.toTimeString is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_toLocaleString(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return asciiString(d.time().Format(datetimeLayout_en_GB)) + } else { + return stringInvalidDate + } + } + panic(r.NewTypeError("Method Date.prototype.toLocaleString is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_toLocaleDateString(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return asciiString(d.time().Format(dateLayout_en_GB)) + } else { + return stringInvalidDate + } + } + panic(r.NewTypeError("Method Date.prototype.toLocaleDateString is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_toLocaleTimeString(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return asciiString(d.time().Format(timeLayout_en_GB)) + } else { + return stringInvalidDate + } + } + panic(r.NewTypeError("Method Date.prototype.toLocaleTimeString is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_valueOf(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(d.msec) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.valueOf is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getTime(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(d.msec) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getTime is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getFullYear(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.time().Year())) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getFullYear is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getUTCFullYear(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.timeUTC().Year())) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getUTCFullYear is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getMonth(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.time().Month()) - 1) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getMonth is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getUTCMonth(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.timeUTC().Month()) - 1) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getUTCMonth is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getHours(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.time().Hour())) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getHours is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getUTCHours(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.timeUTC().Hour())) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getUTCHours is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getDate(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.time().Day())) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getDate is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getUTCDate(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.timeUTC().Day())) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getUTCDate is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getDay(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.time().Weekday())) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getDay is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getUTCDay(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.timeUTC().Weekday())) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getUTCDay is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getMinutes(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.time().Minute())) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getMinutes is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getUTCMinutes(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.timeUTC().Minute())) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getUTCMinutes is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getSeconds(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.time().Second())) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getSeconds is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getUTCSeconds(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.timeUTC().Second())) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getUTCSeconds is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getMilliseconds(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.time().Nanosecond() / 1e6)) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getMilliseconds is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getUTCMilliseconds(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + return intToValue(int64(d.timeUTC().Nanosecond() / 1e6)) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getUTCMilliseconds is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_getTimezoneOffset(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + if d.isSet() { + _, offset := d.time().Zone() + return floatToValue(float64(-offset) / 60) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.getTimezoneOffset is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_setTime(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + n := call.Argument(0).ToNumber() + if IsNaN(n) { + d.unset() + return _NaN + } + return d.setTimeMs(n.ToInteger()) + } + panic(r.NewTypeError("Method Date.prototype.setTime is called on incompatible receiver")) +} + +// _norm returns nhi, nlo such that +// +// hi * base + lo == nhi * base + nlo +// 0 <= nlo < base +func _norm(hi, lo, base int64) (nhi, nlo int64, ok bool) { + if lo < 0 { + if hi == math.MinInt64 && lo <= -base { + // underflow + ok = false + return + } + n := (-lo-1)/base + 1 + hi -= n + lo += n * base + } + if lo >= base { + if hi == math.MaxInt64 { + // overflow + ok = false + return + } + n := lo / base + hi += n + lo -= n * base + } + return hi, lo, true +} + +func mkTime(year, m, day, hour, min, sec, nsec int64, loc *time.Location) (t time.Time, ok bool) { + year, m, ok = _norm(year, m, 12) + if !ok { + return + } + + // Normalise nsec, sec, min, hour, overflowing into day. + sec, nsec, ok = _norm(sec, nsec, 1e9) + if !ok { + return + } + min, sec, ok = _norm(min, sec, 60) + if !ok { + return + } + hour, min, ok = _norm(hour, min, 60) + if !ok { + return + } + day, hour, ok = _norm(day, hour, 24) + if !ok { + return + } + if year > math.MaxInt32 || year < math.MinInt32 || + day > math.MaxInt32 || day < math.MinInt32 || + m >= math.MaxInt32 || m < math.MinInt32-1 { + return time.Time{}, false + } + month := time.Month(m) + 1 + return time.Date(int(year), month, int(day), int(hour), int(min), int(sec), int(nsec), loc), true +} + +func _intArg(call FunctionCall, argNum int) (int64, bool) { + n := call.Argument(argNum).ToNumber() + if IsNaN(n) { + return 0, false + } + return n.ToInteger(), true +} + +func _dateSetYear(t time.Time, call FunctionCall, argNum int, utc bool) (time.Time, bool) { + var year int64 + if argNum == 0 || argNum > 0 && argNum < len(call.Arguments) { + var ok bool + year, ok = _intArg(call, argNum) + if !ok { + return time.Time{}, false + } + if year >= 0 && year <= 99 { + year += 1900 + } + } else { + year = int64(t.Year()) + } + + return _dateSetMonth(year, t, call, argNum+1, utc) +} + +func _dateSetFullYear(t time.Time, call FunctionCall, argNum int, utc bool) (time.Time, bool) { + var year int64 + if argNum == 0 || argNum > 0 && argNum < len(call.Arguments) { + var ok bool + year, ok = _intArg(call, argNum) + if !ok { + return time.Time{}, false + } + } else { + year = int64(t.Year()) + } + return _dateSetMonth(year, t, call, argNum+1, utc) +} + +func _dateSetMonth(year int64, t time.Time, call FunctionCall, argNum int, utc bool) (time.Time, bool) { + var mon int64 + if argNum == 0 || argNum > 0 && argNum < len(call.Arguments) { + var ok bool + mon, ok = _intArg(call, argNum) + if !ok { + return time.Time{}, false + } + } else { + mon = int64(t.Month()) - 1 + } + + return _dateSetDay(year, mon, t, call, argNum+1, utc) +} + +func _dateSetDay(year, mon int64, t time.Time, call FunctionCall, argNum int, utc bool) (time.Time, bool) { + var day int64 + if argNum == 0 || argNum > 0 && argNum < len(call.Arguments) { + var ok bool + day, ok = _intArg(call, argNum) + if !ok { + return time.Time{}, false + } + } else { + day = int64(t.Day()) + } + + return _dateSetHours(year, mon, day, t, call, argNum+1, utc) +} + +func _dateSetHours(year, mon, day int64, t time.Time, call FunctionCall, argNum int, utc bool) (time.Time, bool) { + var hours int64 + if argNum == 0 || argNum > 0 && argNum < len(call.Arguments) { + var ok bool + hours, ok = _intArg(call, argNum) + if !ok { + return time.Time{}, false + } + } else { + hours = int64(t.Hour()) + } + return _dateSetMinutes(year, mon, day, hours, t, call, argNum+1, utc) +} + +func _dateSetMinutes(year, mon, day, hours int64, t time.Time, call FunctionCall, argNum int, utc bool) (time.Time, bool) { + var min int64 + if argNum == 0 || argNum > 0 && argNum < len(call.Arguments) { + var ok bool + min, ok = _intArg(call, argNum) + if !ok { + return time.Time{}, false + } + } else { + min = int64(t.Minute()) + } + return _dateSetSeconds(year, mon, day, hours, min, t, call, argNum+1, utc) +} + +func _dateSetSeconds(year, mon, day, hours, min int64, t time.Time, call FunctionCall, argNum int, utc bool) (time.Time, bool) { + var sec int64 + if argNum == 0 || argNum > 0 && argNum < len(call.Arguments) { + var ok bool + sec, ok = _intArg(call, argNum) + if !ok { + return time.Time{}, false + } + } else { + sec = int64(t.Second()) + } + return _dateSetMilliseconds(year, mon, day, hours, min, sec, t, call, argNum+1, utc) +} + +func _dateSetMilliseconds(year, mon, day, hours, min, sec int64, t time.Time, call FunctionCall, argNum int, utc bool) (time.Time, bool) { + var msec int64 + if argNum == 0 || argNum > 0 && argNum < len(call.Arguments) { + var ok bool + msec, ok = _intArg(call, argNum) + if !ok { + return time.Time{}, false + } + } else { + msec = int64(t.Nanosecond() / 1e6) + } + var ok bool + sec, msec, ok = _norm(sec, msec, 1e3) + if !ok { + return time.Time{}, false + } + + var loc *time.Location + if utc { + loc = time.UTC + } else { + loc = time.Local + } + r, ok := mkTime(year, mon, day, hours, min, sec, msec*1e6, loc) + if !ok { + return time.Time{}, false + } + if utc { + return r.In(time.Local), true + } + return r, true +} + +func (r *Runtime) dateproto_setMilliseconds(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + n := call.Argument(0).ToNumber() + if IsNaN(n) { + d.unset() + return _NaN + } + msec := n.ToInteger() + sec := d.msec / 1e3 + var ok bool + sec, msec, ok = _norm(sec, msec, 1e3) + if !ok { + d.unset() + return _NaN + } + if d.isSet() { + return d.setTimeMs(sec*1e3 + msec) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.setMilliseconds is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_setUTCMilliseconds(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + n := call.Argument(0).ToNumber() + if IsNaN(n) { + d.unset() + return _NaN + } + msec := n.ToInteger() + sec := d.msec / 1e3 + var ok bool + sec, msec, ok = _norm(sec, msec, 1e3) + if !ok { + d.unset() + return _NaN + } + if d.isSet() { + return d.setTimeMs(sec*1e3 + msec) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.setUTCMilliseconds is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_setSeconds(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + t, ok := _dateSetFullYear(d.time(), call, -5, false) + if !ok { + d.unset() + return _NaN + } + if d.isSet() { + return d.setTimeMs(timeToMsec(t)) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.setSeconds is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_setUTCSeconds(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + t, ok := _dateSetFullYear(d.timeUTC(), call, -5, true) + if !ok { + d.unset() + return _NaN + } + if d.isSet() { + return d.setTimeMs(timeToMsec(t)) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.setUTCSeconds is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_setMinutes(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + t, ok := _dateSetFullYear(d.time(), call, -4, false) + if !ok { + d.unset() + return _NaN + } + if d.isSet() { + return d.setTimeMs(timeToMsec(t)) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.setMinutes is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_setUTCMinutes(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + t, ok := _dateSetFullYear(d.timeUTC(), call, -4, true) + if !ok { + d.unset() + return _NaN + } + if d.isSet() { + return d.setTimeMs(timeToMsec(t)) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.setUTCMinutes is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_setHours(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + t, ok := _dateSetFullYear(d.time(), call, -3, false) + if !ok { + d.unset() + return _NaN + } + if d.isSet() { + return d.setTimeMs(timeToMsec(t)) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.setHours is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_setUTCHours(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + t, ok := _dateSetFullYear(d.timeUTC(), call, -3, true) + if !ok { + d.unset() + return _NaN + } + if d.isSet() { + return d.setTimeMs(timeToMsec(t)) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.setUTCHours is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_setDate(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + t, ok := _dateSetFullYear(d.time(), limitCallArgs(call, 1), -2, false) + if !ok { + d.unset() + return _NaN + } + if d.isSet() { + return d.setTimeMs(timeToMsec(t)) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.setDate is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_setUTCDate(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + t, ok := _dateSetFullYear(d.timeUTC(), limitCallArgs(call, 1), -2, true) + if !ok { + d.unset() + return _NaN + } + if d.isSet() { + return d.setTimeMs(timeToMsec(t)) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.setUTCDate is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_setMonth(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + t, ok := _dateSetFullYear(d.time(), limitCallArgs(call, 2), -1, false) + if !ok { + d.unset() + return _NaN + } + if d.isSet() { + return d.setTimeMs(timeToMsec(t)) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.setMonth is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_setUTCMonth(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + t, ok := _dateSetFullYear(d.timeUTC(), limitCallArgs(call, 2), -1, true) + if !ok { + d.unset() + return _NaN + } + if d.isSet() { + return d.setTimeMs(timeToMsec(t)) + } else { + return _NaN + } + } + panic(r.NewTypeError("Method Date.prototype.setUTCMonth is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_setFullYear(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + var t time.Time + if d.isSet() { + t = d.time() + } else { + t = time.Date(1970, time.January, 1, 0, 0, 0, 0, time.Local) + } + t, ok := _dateSetFullYear(t, limitCallArgs(call, 3), 0, false) + if !ok { + d.unset() + return _NaN + } + return d.setTimeMs(timeToMsec(t)) + } + panic(r.NewTypeError("Method Date.prototype.setFullYear is called on incompatible receiver")) +} + +func (r *Runtime) dateproto_setUTCFullYear(call FunctionCall) Value { + obj := r.toObject(call.This) + if d, ok := obj.self.(*dateObject); ok { + var t time.Time + if d.isSet() { + t = d.timeUTC() + } else { + t = time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC) + } + t, ok := _dateSetFullYear(t, limitCallArgs(call, 3), 0, true) + if !ok { + d.unset() + return _NaN + } + return d.setTimeMs(timeToMsec(t)) + } + panic(r.NewTypeError("Method Date.prototype.setUTCFullYear is called on incompatible receiver")) +} + +var dateTemplate *objectTemplate +var dateTemplateOnce sync.Once + +func getDateTemplate() *objectTemplate { + dateTemplateOnce.Do(func() { + dateTemplate = createDateTemplate() + }) + return dateTemplate +} + +func createDateTemplate() *objectTemplate { + t := newObjectTemplate() + t.protoFactory = func(r *Runtime) *Object { + return r.getFunctionPrototype() + } + + t.putStr("name", func(r *Runtime) Value { return valueProp(asciiString("Date"), false, false, true) }) + t.putStr("length", func(r *Runtime) Value { return valueProp(intToValue(7), false, false, true) }) + + t.putStr("prototype", func(r *Runtime) Value { return valueProp(r.getDatePrototype(), false, false, false) }) + + t.putStr("parse", func(r *Runtime) Value { return r.methodProp(r.date_parse, "parse", 1) }) + t.putStr("UTC", func(r *Runtime) Value { return r.methodProp(r.date_UTC, "UTC", 7) }) + t.putStr("now", func(r *Runtime) Value { return r.methodProp(r.date_now, "now", 0) }) + + return t +} + +func (r *Runtime) getDate() *Object { + ret := r.global.Date + if ret == nil { + ret = &Object{runtime: r} + r.global.Date = ret + r.newTemplatedFuncObject(getDateTemplate(), ret, r.builtin_date, + r.wrapNativeConstruct(r.builtin_newDate, ret, r.getDatePrototype())) + } + return ret +} + +func createDateProtoTemplate() *objectTemplate { + t := newObjectTemplate() + t.protoFactory = func(r *Runtime) *Object { + return r.global.ObjectPrototype + } + + t.putStr("constructor", func(r *Runtime) Value { return valueProp(r.getDate(), true, false, true) }) + + t.putStr("toString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toString, "toString", 0) }) + t.putStr("toDateString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toDateString, "toDateString", 0) }) + t.putStr("toTimeString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toTimeString, "toTimeString", 0) }) + t.putStr("toLocaleString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toLocaleString, "toLocaleString", 0) }) + t.putStr("toLocaleDateString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toLocaleDateString, "toLocaleDateString", 0) }) + t.putStr("toLocaleTimeString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toLocaleTimeString, "toLocaleTimeString", 0) }) + t.putStr("valueOf", func(r *Runtime) Value { return r.methodProp(r.dateproto_valueOf, "valueOf", 0) }) + t.putStr("getTime", func(r *Runtime) Value { return r.methodProp(r.dateproto_getTime, "getTime", 0) }) + t.putStr("getFullYear", func(r *Runtime) Value { return r.methodProp(r.dateproto_getFullYear, "getFullYear", 0) }) + t.putStr("getUTCFullYear", func(r *Runtime) Value { return r.methodProp(r.dateproto_getUTCFullYear, "getUTCFullYear", 0) }) + t.putStr("getMonth", func(r *Runtime) Value { return r.methodProp(r.dateproto_getMonth, "getMonth", 0) }) + t.putStr("getUTCMonth", func(r *Runtime) Value { return r.methodProp(r.dateproto_getUTCMonth, "getUTCMonth", 0) }) + t.putStr("getDate", func(r *Runtime) Value { return r.methodProp(r.dateproto_getDate, "getDate", 0) }) + t.putStr("getUTCDate", func(r *Runtime) Value { return r.methodProp(r.dateproto_getUTCDate, "getUTCDate", 0) }) + t.putStr("getDay", func(r *Runtime) Value { return r.methodProp(r.dateproto_getDay, "getDay", 0) }) + t.putStr("getUTCDay", func(r *Runtime) Value { return r.methodProp(r.dateproto_getUTCDay, "getUTCDay", 0) }) + t.putStr("getHours", func(r *Runtime) Value { return r.methodProp(r.dateproto_getHours, "getHours", 0) }) + t.putStr("getUTCHours", func(r *Runtime) Value { return r.methodProp(r.dateproto_getUTCHours, "getUTCHours", 0) }) + t.putStr("getMinutes", func(r *Runtime) Value { return r.methodProp(r.dateproto_getMinutes, "getMinutes", 0) }) + t.putStr("getUTCMinutes", func(r *Runtime) Value { return r.methodProp(r.dateproto_getUTCMinutes, "getUTCMinutes", 0) }) + t.putStr("getSeconds", func(r *Runtime) Value { return r.methodProp(r.dateproto_getSeconds, "getSeconds", 0) }) + t.putStr("getUTCSeconds", func(r *Runtime) Value { return r.methodProp(r.dateproto_getUTCSeconds, "getUTCSeconds", 0) }) + t.putStr("getMilliseconds", func(r *Runtime) Value { return r.methodProp(r.dateproto_getMilliseconds, "getMilliseconds", 0) }) + t.putStr("getUTCMilliseconds", func(r *Runtime) Value { return r.methodProp(r.dateproto_getUTCMilliseconds, "getUTCMilliseconds", 0) }) + t.putStr("getTimezoneOffset", func(r *Runtime) Value { return r.methodProp(r.dateproto_getTimezoneOffset, "getTimezoneOffset", 0) }) + t.putStr("setTime", func(r *Runtime) Value { return r.methodProp(r.dateproto_setTime, "setTime", 1) }) + t.putStr("setMilliseconds", func(r *Runtime) Value { return r.methodProp(r.dateproto_setMilliseconds, "setMilliseconds", 1) }) + t.putStr("setUTCMilliseconds", func(r *Runtime) Value { return r.methodProp(r.dateproto_setUTCMilliseconds, "setUTCMilliseconds", 1) }) + t.putStr("setSeconds", func(r *Runtime) Value { return r.methodProp(r.dateproto_setSeconds, "setSeconds", 2) }) + t.putStr("setUTCSeconds", func(r *Runtime) Value { return r.methodProp(r.dateproto_setUTCSeconds, "setUTCSeconds", 2) }) + t.putStr("setMinutes", func(r *Runtime) Value { return r.methodProp(r.dateproto_setMinutes, "setMinutes", 3) }) + t.putStr("setUTCMinutes", func(r *Runtime) Value { return r.methodProp(r.dateproto_setUTCMinutes, "setUTCMinutes", 3) }) + t.putStr("setHours", func(r *Runtime) Value { return r.methodProp(r.dateproto_setHours, "setHours", 4) }) + t.putStr("setUTCHours", func(r *Runtime) Value { return r.methodProp(r.dateproto_setUTCHours, "setUTCHours", 4) }) + t.putStr("setDate", func(r *Runtime) Value { return r.methodProp(r.dateproto_setDate, "setDate", 1) }) + t.putStr("setUTCDate", func(r *Runtime) Value { return r.methodProp(r.dateproto_setUTCDate, "setUTCDate", 1) }) + t.putStr("setMonth", func(r *Runtime) Value { return r.methodProp(r.dateproto_setMonth, "setMonth", 2) }) + t.putStr("setUTCMonth", func(r *Runtime) Value { return r.methodProp(r.dateproto_setUTCMonth, "setUTCMonth", 2) }) + t.putStr("setFullYear", func(r *Runtime) Value { return r.methodProp(r.dateproto_setFullYear, "setFullYear", 3) }) + t.putStr("setUTCFullYear", func(r *Runtime) Value { return r.methodProp(r.dateproto_setUTCFullYear, "setUTCFullYear", 3) }) + t.putStr("toUTCString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toUTCString, "toUTCString", 0) }) + t.putStr("toISOString", func(r *Runtime) Value { return r.methodProp(r.dateproto_toISOString, "toISOString", 0) }) + t.putStr("toJSON", func(r *Runtime) Value { return r.methodProp(r.dateproto_toJSON, "toJSON", 1) }) + + t.putSym(SymToPrimitive, func(r *Runtime) Value { + return valueProp(r.newNativeFunc(r.dateproto_toPrimitive, "[Symbol.toPrimitive]", 1), false, false, true) + }) + + return t +} + +var dateProtoTemplate *objectTemplate +var dateProtoTemplateOnce sync.Once + +func getDateProtoTemplate() *objectTemplate { + dateProtoTemplateOnce.Do(func() { + dateProtoTemplate = createDateProtoTemplate() + }) + return dateProtoTemplate +} + +func (r *Runtime) getDatePrototype() *Object { + ret := r.global.DatePrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.DatePrototype = ret + r.newTemplatedObject(getDateProtoTemplate(), ret) + } + return ret +} diff --git a/goja/builtin_error.go b/goja/builtin_error.go new file mode 100644 index 0000000..a7eae7d --- /dev/null +++ b/goja/builtin_error.go @@ -0,0 +1,314 @@ +package goja + +import "github.com/dop251/goja/unistring" + +const propNameStack = "stack" + +type errorObject struct { + baseObject + stack []StackFrame + stackPropAdded bool +} + +func (e *errorObject) formatStack() String { + var b StringBuilder + val := writeErrorString(&b, e.val) + if val != nil { + b.WriteString(val) + } + b.WriteRune('\n') + + for _, frame := range e.stack { + b.writeASCII("\tat ") + frame.WriteToValueBuilder(&b) + b.WriteRune('\n') + } + return b.String() +} + +func (e *errorObject) addStackProp() Value { + if !e.stackPropAdded { + res := e._putProp(propNameStack, e.formatStack(), true, false, true) + if len(e.propNames) > 1 { + // reorder property names to ensure 'stack' is the first one + copy(e.propNames[1:], e.propNames) + e.propNames[0] = propNameStack + } + e.stackPropAdded = true + return res + } + return nil +} + +func (e *errorObject) getStr(p unistring.String, receiver Value) Value { + return e.getStrWithOwnProp(e.getOwnPropStr(p), p, receiver) +} + +func (e *errorObject) getOwnPropStr(name unistring.String) Value { + res := e.baseObject.getOwnPropStr(name) + if res == nil && name == propNameStack { + return e.addStackProp() + } + + return res +} + +func (e *errorObject) setOwnStr(name unistring.String, val Value, throw bool) bool { + if name == propNameStack { + e.addStackProp() + } + return e.baseObject.setOwnStr(name, val, throw) +} + +func (e *errorObject) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { + return e._setForeignStr(name, e.getOwnPropStr(name), val, receiver, throw) +} + +func (e *errorObject) deleteStr(name unistring.String, throw bool) bool { + if name == propNameStack { + e.addStackProp() + } + return e.baseObject.deleteStr(name, throw) +} + +func (e *errorObject) defineOwnPropertyStr(name unistring.String, desc PropertyDescriptor, throw bool) bool { + if name == propNameStack { + e.addStackProp() + } + return e.baseObject.defineOwnPropertyStr(name, desc, throw) +} + +func (e *errorObject) hasOwnPropertyStr(name unistring.String) bool { + if e.baseObject.hasOwnPropertyStr(name) { + return true + } + + return name == propNameStack && !e.stackPropAdded +} + +func (e *errorObject) stringKeys(all bool, accum []Value) []Value { + if all && !e.stackPropAdded { + accum = append(accum, asciiString(propNameStack)) + } + return e.baseObject.stringKeys(all, accum) +} + +func (e *errorObject) iterateStringKeys() iterNextFunc { + e.addStackProp() + return e.baseObject.iterateStringKeys() +} + +func (e *errorObject) init() { + e.baseObject.init() + vm := e.val.runtime.vm + e.stack = vm.captureStack(make([]StackFrame, 0, len(vm.callStack)+1), 0) +} + +func (r *Runtime) newErrorObject(proto *Object, class string) *errorObject { + obj := &Object{runtime: r} + o := &errorObject{ + baseObject: baseObject{ + class: class, + val: obj, + extensible: true, + prototype: proto, + }, + } + obj.self = o + o.init() + return o +} + +func (r *Runtime) builtin_Error(args []Value, proto *Object) *Object { + obj := r.newErrorObject(proto, classError) + if len(args) > 0 && args[0] != _undefined { + obj._putProp("message", args[0].ToString(), true, false, true) + } + if len(args) > 1 && args[1] != _undefined { + if options, ok := args[1].(*Object); ok { + if options.hasProperty(asciiString("cause")) { + obj.defineOwnPropertyStr("cause", PropertyDescriptor{ + Writable: FLAG_TRUE, + Enumerable: FLAG_FALSE, + Configurable: FLAG_TRUE, + Value: options.Get("cause"), + }, true) + } + } + } + return obj.val +} + +func (r *Runtime) builtin_AggregateError(args []Value, proto *Object) *Object { + obj := r.newErrorObject(proto, classError) + if len(args) > 1 && args[1] != nil && args[1] != _undefined { + obj._putProp("message", args[1].toString(), true, false, true) + } + var errors []Value + if len(args) > 0 { + errors = r.iterableToList(args[0], nil) + } + obj._putProp("errors", r.newArrayValues(errors), true, false, true) + + if len(args) > 2 && args[2] != _undefined { + if options, ok := args[2].(*Object); ok { + if options.hasProperty(asciiString("cause")) { + obj.defineOwnPropertyStr("cause", PropertyDescriptor{ + Writable: FLAG_TRUE, + Enumerable: FLAG_FALSE, + Configurable: FLAG_TRUE, + Value: options.Get("cause"), + }, true) + } + } + } + + return obj.val +} + +func writeErrorString(sb *StringBuilder, obj *Object) String { + var nameStr, msgStr String + name := obj.self.getStr("name", nil) + if name == nil || name == _undefined { + nameStr = asciiString("Error") + } else { + nameStr = name.toString() + } + msg := obj.self.getStr("message", nil) + if msg == nil || msg == _undefined { + msgStr = stringEmpty + } else { + msgStr = msg.toString() + } + if nameStr.Length() == 0 { + return msgStr + } + if msgStr.Length() == 0 { + return nameStr + } + sb.WriteString(nameStr) + sb.WriteString(asciiString(": ")) + sb.WriteString(msgStr) + return nil +} + +func (r *Runtime) error_toString(call FunctionCall) Value { + var sb StringBuilder + val := writeErrorString(&sb, r.toObject(call.This)) + if val != nil { + return val + } + return sb.String() +} + +func (r *Runtime) createErrorPrototype(name String, ctor *Object) *Object { + o := r.newBaseObject(r.getErrorPrototype(), classObject) + o._putProp("message", stringEmpty, true, false, true) + o._putProp("name", name, true, false, true) + o._putProp("constructor", ctor, true, false, true) + return o.val +} + +func (r *Runtime) getErrorPrototype() *Object { + ret := r.global.ErrorPrototype + if ret == nil { + ret = r.NewObject() + r.global.ErrorPrototype = ret + o := ret.self + o._putProp("message", stringEmpty, true, false, true) + o._putProp("name", stringError, true, false, true) + o._putProp("toString", r.newNativeFunc(r.error_toString, "toString", 0), true, false, true) + o._putProp("constructor", r.getError(), true, false, true) + } + return ret +} + +func (r *Runtime) getError() *Object { + ret := r.global.Error + if ret == nil { + ret = &Object{runtime: r} + r.global.Error = ret + r.newNativeFuncConstruct(ret, r.builtin_Error, "Error", r.getErrorPrototype(), 1) + } + return ret +} + +func (r *Runtime) getAggregateError() *Object { + ret := r.global.AggregateError + if ret == nil { + ret = &Object{runtime: r} + r.global.AggregateError = ret + r.newNativeFuncConstructProto(ret, r.builtin_AggregateError, "AggregateError", r.createErrorPrototype(stringAggregateError, ret), r.getError(), 2) + } + return ret +} + +func (r *Runtime) getTypeError() *Object { + ret := r.global.TypeError + if ret == nil { + ret = &Object{runtime: r} + r.global.TypeError = ret + r.newNativeFuncConstructProto(ret, r.builtin_Error, "TypeError", r.createErrorPrototype(stringTypeError, ret), r.getError(), 1) + } + return ret +} + +func (r *Runtime) getReferenceError() *Object { + ret := r.global.ReferenceError + if ret == nil { + ret = &Object{runtime: r} + r.global.ReferenceError = ret + r.newNativeFuncConstructProto(ret, r.builtin_Error, "ReferenceError", r.createErrorPrototype(stringReferenceError, ret), r.getError(), 1) + } + return ret +} + +func (r *Runtime) getSyntaxError() *Object { + ret := r.global.SyntaxError + if ret == nil { + ret = &Object{runtime: r} + r.global.SyntaxError = ret + r.newNativeFuncConstructProto(ret, r.builtin_Error, "SyntaxError", r.createErrorPrototype(stringSyntaxError, ret), r.getError(), 1) + } + return ret +} + +func (r *Runtime) getRangeError() *Object { + ret := r.global.RangeError + if ret == nil { + ret = &Object{runtime: r} + r.global.RangeError = ret + r.newNativeFuncConstructProto(ret, r.builtin_Error, "RangeError", r.createErrorPrototype(stringRangeError, ret), r.getError(), 1) + } + return ret +} + +func (r *Runtime) getEvalError() *Object { + ret := r.global.EvalError + if ret == nil { + ret = &Object{runtime: r} + r.global.EvalError = ret + r.newNativeFuncConstructProto(ret, r.builtin_Error, "EvalError", r.createErrorPrototype(stringEvalError, ret), r.getError(), 1) + } + return ret +} + +func (r *Runtime) getURIError() *Object { + ret := r.global.URIError + if ret == nil { + ret = &Object{runtime: r} + r.global.URIError = ret + r.newNativeFuncConstructProto(ret, r.builtin_Error, "URIError", r.createErrorPrototype(stringURIError, ret), r.getError(), 1) + } + return ret +} + +func (r *Runtime) getGoError() *Object { + ret := r.global.GoError + if ret == nil { + ret = &Object{runtime: r} + r.global.GoError = ret + r.newNativeFuncConstructProto(ret, r.builtin_Error, "GoError", r.createErrorPrototype(stringGoError, ret), r.getError(), 1) + } + return ret +} diff --git a/goja/builtin_function.go b/goja/builtin_function.go new file mode 100644 index 0000000..26a1287 --- /dev/null +++ b/goja/builtin_function.go @@ -0,0 +1,416 @@ +package goja + +import ( + "math" + "sync" +) + +func (r *Runtime) functionCtor(args []Value, proto *Object, async, generator bool) *Object { + var sb StringBuilder + if async { + if generator { + sb.WriteString(asciiString("(async function* anonymous(")) + } else { + sb.WriteString(asciiString("(async function anonymous(")) + } + } else { + if generator { + sb.WriteString(asciiString("(function* anonymous(")) + } else { + sb.WriteString(asciiString("(function anonymous(")) + } + } + if len(args) > 1 { + ar := args[:len(args)-1] + for i, arg := range ar { + sb.WriteString(arg.toString()) + if i < len(ar)-1 { + sb.WriteRune(',') + } + } + } + sb.WriteString(asciiString("\n) {\n")) + if len(args) > 0 { + sb.WriteString(args[len(args)-1].toString()) + } + sb.WriteString(asciiString("\n})")) + + ret := r.toObject(r.eval(sb.String(), false, false)) + ret.self.setProto(proto, true) + return ret +} + +func (r *Runtime) builtin_Function(args []Value, proto *Object) *Object { + return r.functionCtor(args, proto, false, false) +} + +func (r *Runtime) builtin_asyncFunction(args []Value, proto *Object) *Object { + return r.functionCtor(args, proto, true, false) +} + +func (r *Runtime) builtin_generatorFunction(args []Value, proto *Object) *Object { + return r.functionCtor(args, proto, false, true) +} + +func (r *Runtime) functionproto_toString(call FunctionCall) Value { + obj := r.toObject(call.This) + switch f := obj.self.(type) { + case funcObjectImpl: + return f.source() + case *proxyObject: + if _, ok := f.target.self.(funcObjectImpl); ok { + return asciiString("function () { [native code] }") + } + } + panic(r.NewTypeError("Function.prototype.toString requires that 'this' be a Function")) +} + +func (r *Runtime) functionproto_hasInstance(call FunctionCall) Value { + if o, ok := call.This.(*Object); ok { + if _, ok = o.self.assertCallable(); ok { + return r.toBoolean(o.self.hasInstance(call.Argument(0))) + } + } + + return valueFalse +} + +func (r *Runtime) createListFromArrayLike(a Value) []Value { + o := r.toObject(a) + if arr := r.checkStdArrayObj(o); arr != nil { + return arr.values + } + l := toLength(o.self.getStr("length", nil)) + res := make([]Value, 0, l) + for k := int64(0); k < l; k++ { + res = append(res, nilSafe(o.self.getIdx(valueInt(k), nil))) + } + return res +} + +func (r *Runtime) functionproto_apply(call FunctionCall) Value { + var args []Value + if len(call.Arguments) >= 2 { + args = r.createListFromArrayLike(call.Arguments[1]) + } + + f := r.toCallable(call.This) + return f(FunctionCall{ + This: call.Argument(0), + Arguments: args, + }) +} + +func (r *Runtime) functionproto_call(call FunctionCall) Value { + var args []Value + if len(call.Arguments) > 0 { + args = call.Arguments[1:] + } + + f := r.toCallable(call.This) + return f(FunctionCall{ + This: call.Argument(0), + Arguments: args, + }) +} + +func (r *Runtime) boundCallable(target func(FunctionCall) Value, boundArgs []Value) func(FunctionCall) Value { + var this Value + var args []Value + if len(boundArgs) > 0 { + this = boundArgs[0] + args = make([]Value, len(boundArgs)-1) + copy(args, boundArgs[1:]) + } else { + this = _undefined + } + return func(call FunctionCall) Value { + a := append(args, call.Arguments...) + return target(FunctionCall{ + This: this, + Arguments: a, + }) + } +} + +func (r *Runtime) boundConstruct(f *Object, target func([]Value, *Object) *Object, boundArgs []Value) func([]Value, *Object) *Object { + if target == nil { + return nil + } + var args []Value + if len(boundArgs) > 1 { + args = make([]Value, len(boundArgs)-1) + copy(args, boundArgs[1:]) + } + return func(fargs []Value, newTarget *Object) *Object { + a := append(args, fargs...) + if newTarget == f { + newTarget = nil + } + return target(a, newTarget) + } +} + +func (r *Runtime) functionproto_bind(call FunctionCall) Value { + obj := r.toObject(call.This) + + fcall := r.toCallable(call.This) + construct := obj.self.assertConstructor() + + var l = _positiveZero + if obj.self.hasOwnPropertyStr("length") { + var li int64 + switch lenProp := nilSafe(obj.self.getStr("length", nil)).(type) { + case valueInt: + li = lenProp.ToInteger() + case valueFloat: + switch lenProp { + case _positiveInf: + l = lenProp + goto lenNotInt + case _negativeInf: + goto lenNotInt + case _negativeZero: + // no-op, li == 0 + default: + if !math.IsNaN(float64(lenProp)) { + li = int64(math.Abs(float64(lenProp))) + } // else li = 0 + } + } + if len(call.Arguments) > 1 { + li -= int64(len(call.Arguments)) - 1 + } + if li < 0 { + li = 0 + } + l = intToValue(li) + } +lenNotInt: + name := obj.self.getStr("name", nil) + nameStr := stringBound_ + if s, ok := name.(String); ok { + nameStr = nameStr.Concat(s) + } + + v := &Object{runtime: r} + ff := r.newNativeFuncAndConstruct(v, r.boundCallable(fcall, call.Arguments), r.boundConstruct(v, construct, call.Arguments), nil, nameStr.string(), l) + bf := &boundFuncObject{ + nativeFuncObject: *ff, + wrapped: obj, + } + bf.prototype = obj.self.proto() + v.self = bf + + return v +} + +func (r *Runtime) getThrower() *Object { + ret := r.global.thrower + if ret == nil { + ret = r.newNativeFunc(r.builtin_thrower, "", 0) + r.global.thrower = ret + r.object_freeze(FunctionCall{Arguments: []Value{ret}}) + } + return ret +} + +func (r *Runtime) newThrowerProperty(configurable bool) Value { + thrower := r.getThrower() + return &valueProperty{ + getterFunc: thrower, + setterFunc: thrower, + accessor: true, + configurable: configurable, + } +} + +func createFunctionProtoTemplate() *objectTemplate { + t := newObjectTemplate() + t.protoFactory = func(r *Runtime) *Object { + return r.global.ObjectPrototype + } + + t.putStr("constructor", func(r *Runtime) Value { return valueProp(r.getFunction(), true, false, true) }) + + t.putStr("length", func(r *Runtime) Value { return valueProp(_positiveZero, false, false, true) }) + t.putStr("name", func(r *Runtime) Value { return valueProp(stringEmpty, false, false, true) }) + + t.putStr("apply", func(r *Runtime) Value { return r.methodProp(r.functionproto_apply, "apply", 2) }) + t.putStr("bind", func(r *Runtime) Value { return r.methodProp(r.functionproto_bind, "bind", 1) }) + t.putStr("call", func(r *Runtime) Value { return r.methodProp(r.functionproto_call, "call", 1) }) + t.putStr("toString", func(r *Runtime) Value { return r.methodProp(r.functionproto_toString, "toString", 0) }) + + t.putStr("caller", func(r *Runtime) Value { return r.newThrowerProperty(true) }) + t.putStr("arguments", func(r *Runtime) Value { return r.newThrowerProperty(true) }) + + t.putSym(SymHasInstance, func(r *Runtime) Value { + return valueProp(r.newNativeFunc(r.functionproto_hasInstance, "[Symbol.hasInstance]", 1), false, false, false) + }) + + return t +} + +var functionProtoTemplate *objectTemplate +var functionProtoTemplateOnce sync.Once + +func getFunctionProtoTemplate() *objectTemplate { + functionProtoTemplateOnce.Do(func() { + functionProtoTemplate = createFunctionProtoTemplate() + }) + return functionProtoTemplate +} + +func (r *Runtime) getFunctionPrototype() *Object { + ret := r.global.FunctionPrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.FunctionPrototype = ret + r.newTemplatedFuncObject(getFunctionProtoTemplate(), ret, func(FunctionCall) Value { + return _undefined + }, nil) + } + return ret +} + +func (r *Runtime) createFunction(v *Object) objectImpl { + return r.newNativeFuncConstructObj(v, r.builtin_Function, "Function", r.getFunctionPrototype(), 1) +} + +func (r *Runtime) createAsyncFunctionProto(val *Object) objectImpl { + o := &baseObject{ + class: classObject, + val: val, + extensible: true, + prototype: r.getFunctionPrototype(), + } + o.init() + + o._putProp("constructor", r.getAsyncFunction(), true, false, true) + + o._putSym(SymToStringTag, valueProp(asciiString(classAsyncFunction), false, false, true)) + + return o +} + +func (r *Runtime) getAsyncFunctionPrototype() *Object { + var o *Object + if o = r.global.AsyncFunctionPrototype; o == nil { + o = &Object{runtime: r} + r.global.AsyncFunctionPrototype = o + o.self = r.createAsyncFunctionProto(o) + } + return o +} + +func (r *Runtime) createAsyncFunction(val *Object) objectImpl { + o := r.newNativeFuncConstructObj(val, r.builtin_asyncFunction, "AsyncFunction", r.getAsyncFunctionPrototype(), 1) + + return o +} + +func (r *Runtime) getAsyncFunction() *Object { + var o *Object + if o = r.global.AsyncFunction; o == nil { + o = &Object{runtime: r} + r.global.AsyncFunction = o + o.self = r.createAsyncFunction(o) + } + return o +} + +func (r *Runtime) builtin_genproto_next(call FunctionCall) Value { + if o, ok := call.This.(*Object); ok { + if gen, ok := o.self.(*generatorObject); ok { + return gen.next(call.Argument(0)) + } + } + panic(r.NewTypeError("Method [Generator].prototype.next called on incompatible receiver")) +} + +func (r *Runtime) builtin_genproto_return(call FunctionCall) Value { + if o, ok := call.This.(*Object); ok { + if gen, ok := o.self.(*generatorObject); ok { + return gen._return(call.Argument(0)) + } + } + panic(r.NewTypeError("Method [Generator].prototype.return called on incompatible receiver")) +} + +func (r *Runtime) builtin_genproto_throw(call FunctionCall) Value { + if o, ok := call.This.(*Object); ok { + if gen, ok := o.self.(*generatorObject); ok { + return gen.throw(call.Argument(0)) + } + } + panic(r.NewTypeError("Method [Generator].prototype.throw called on incompatible receiver")) +} + +func (r *Runtime) createGeneratorFunctionProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.getFunctionPrototype(), classObject) + + o._putProp("constructor", r.getGeneratorFunction(), false, false, true) + o._putProp("prototype", r.getGeneratorPrototype(), false, false, true) + o._putSym(SymToStringTag, valueProp(asciiString(classGeneratorFunction), false, false, true)) + + return o +} + +func (r *Runtime) getGeneratorFunctionPrototype() *Object { + var o *Object + if o = r.global.GeneratorFunctionPrototype; o == nil { + o = &Object{runtime: r} + r.global.GeneratorFunctionPrototype = o + o.self = r.createGeneratorFunctionProto(o) + } + return o +} + +func (r *Runtime) createGeneratorFunction(val *Object) objectImpl { + o := r.newNativeFuncConstructObj(val, r.builtin_generatorFunction, "GeneratorFunction", r.getGeneratorFunctionPrototype(), 1) + return o +} + +func (r *Runtime) getGeneratorFunction() *Object { + var o *Object + if o = r.global.GeneratorFunction; o == nil { + o = &Object{runtime: r} + r.global.GeneratorFunction = o + o.self = r.createGeneratorFunction(o) + } + return o +} + +func (r *Runtime) createGeneratorProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.getIteratorPrototype(), classObject) + + o._putProp("constructor", r.getGeneratorFunctionPrototype(), false, false, true) + o._putProp("next", r.newNativeFunc(r.builtin_genproto_next, "next", 1), true, false, true) + o._putProp("return", r.newNativeFunc(r.builtin_genproto_return, "return", 1), true, false, true) + o._putProp("throw", r.newNativeFunc(r.builtin_genproto_throw, "throw", 1), true, false, true) + + o._putSym(SymToStringTag, valueProp(asciiString(classGenerator), false, false, true)) + + return o +} + +func (r *Runtime) getGeneratorPrototype() *Object { + var o *Object + if o = r.global.GeneratorPrototype; o == nil { + o = &Object{runtime: r} + r.global.GeneratorPrototype = o + o.self = r.createGeneratorProto(o) + } + return o +} + +func (r *Runtime) getFunction() *Object { + ret := r.global.Function + if ret == nil { + ret = &Object{runtime: r} + r.global.Function = ret + ret.self = r.createFunction(ret) + } + + return ret +} diff --git a/goja/builtin_function_test.go b/goja/builtin_function_test.go new file mode 100644 index 0000000..f330821 --- /dev/null +++ b/goja/builtin_function_test.go @@ -0,0 +1,14 @@ +package goja + +import ( + "testing" +) + +func TestHashbangInFunctionConstructor(t *testing.T) { + const SCRIPT = ` + assert.throws(SyntaxError, function() { + new Function("#!") + }); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} diff --git a/goja/builtin_global.go b/goja/builtin_global.go new file mode 100644 index 0000000..2c6385a --- /dev/null +++ b/goja/builtin_global.go @@ -0,0 +1,576 @@ +package goja + +import ( + "errors" + "io" + "math" + "regexp" + "strconv" + "strings" + "sync" + "unicode/utf8" + + "github.com/dop251/goja/unistring" +) + +const hexUpper = "0123456789ABCDEF" + +var ( + parseFloatRegexp = regexp.MustCompile(`^([+-]?(?:Infinity|[0-9]*\.?[0-9]*(?:[eE][+-]?[0-9]+)?))`) +) + +func (r *Runtime) builtin_isNaN(call FunctionCall) Value { + if math.IsNaN(call.Argument(0).ToFloat()) { + return valueTrue + } else { + return valueFalse + } +} + +func (r *Runtime) builtin_parseInt(call FunctionCall) Value { + str := call.Argument(0).toString().toTrimmedUTF8() + radix := int(toInt32(call.Argument(1))) + v, _ := parseInt(str, radix) + return v +} + +func (r *Runtime) builtin_parseFloat(call FunctionCall) Value { + m := parseFloatRegexp.FindStringSubmatch(call.Argument(0).toString().toTrimmedUTF8()) + if len(m) == 2 { + if s := m[1]; s != "" && s != "+" && s != "-" { + switch s { + case "+", "-": + case "Infinity", "+Infinity": + return _positiveInf + case "-Infinity": + return _negativeInf + default: + f, err := strconv.ParseFloat(s, 64) + if err == nil || isRangeErr(err) { + return floatToValue(f) + } + } + } + } + return _NaN +} + +func (r *Runtime) builtin_isFinite(call FunctionCall) Value { + f := call.Argument(0).ToFloat() + if math.IsNaN(f) || math.IsInf(f, 0) { + return valueFalse + } + return valueTrue +} + +func (r *Runtime) _encode(uriString String, unescaped *[256]bool) String { + reader := uriString.Reader() + utf8Buf := make([]byte, utf8.UTFMax) + needed := false + l := 0 + for { + rn, _, err := reader.ReadRune() + if err != nil { + if err != io.EOF { + panic(r.newError(r.getURIError(), "Malformed URI")) + } + break + } + + if rn >= utf8.RuneSelf { + needed = true + l += utf8.EncodeRune(utf8Buf, rn) * 3 + } else if !unescaped[rn] { + needed = true + l += 3 + } else { + l++ + } + } + + if !needed { + return uriString + } + + buf := make([]byte, l) + i := 0 + reader = uriString.Reader() + for { + rn, _, err := reader.ReadRune() + if err == io.EOF { + break + } + + if rn >= utf8.RuneSelf { + n := utf8.EncodeRune(utf8Buf, rn) + for _, b := range utf8Buf[:n] { + buf[i] = '%' + buf[i+1] = hexUpper[b>>4] + buf[i+2] = hexUpper[b&15] + i += 3 + } + } else if !unescaped[rn] { + buf[i] = '%' + buf[i+1] = hexUpper[rn>>4] + buf[i+2] = hexUpper[rn&15] + i += 3 + } else { + buf[i] = byte(rn) + i++ + } + } + return asciiString(buf) +} + +func (r *Runtime) _decode(sv String, reservedSet *[256]bool) String { + s := sv.String() + hexCount := 0 + for i := 0; i < len(s); { + switch s[i] { + case '%': + if i+2 >= len(s) || !ishex(s[i+1]) || !ishex(s[i+2]) { + panic(r.newError(r.getURIError(), "Malformed URI")) + } + c := unhex(s[i+1])<<4 | unhex(s[i+2]) + if !reservedSet[c] { + hexCount++ + } + i += 3 + default: + i++ + } + } + + if hexCount == 0 { + return sv + } + + t := make([]byte, len(s)-hexCount*2) + j := 0 + isUnicode := false + for i := 0; i < len(s); { + ch := s[i] + switch ch { + case '%': + c := unhex(s[i+1])<<4 | unhex(s[i+2]) + if reservedSet[c] { + t[j] = s[i] + t[j+1] = s[i+1] + t[j+2] = s[i+2] + j += 3 + } else { + t[j] = c + if c >= utf8.RuneSelf { + isUnicode = true + } + j++ + } + i += 3 + default: + if ch >= utf8.RuneSelf { + isUnicode = true + } + t[j] = ch + j++ + i++ + } + } + + if !isUnicode { + return asciiString(t) + } + + us := make([]rune, 0, len(s)) + for len(t) > 0 { + rn, size := utf8.DecodeRune(t) + if rn == utf8.RuneError { + if size != 3 || t[0] != 0xef || t[1] != 0xbf || t[2] != 0xbd { + panic(r.newError(r.getURIError(), "Malformed URI")) + } + } + us = append(us, rn) + t = t[size:] + } + return unicodeStringFromRunes(us) +} + +func ishex(c byte) bool { + switch { + case '0' <= c && c <= '9': + return true + case 'a' <= c && c <= 'f': + return true + case 'A' <= c && c <= 'F': + return true + } + return false +} + +func unhex(c byte) byte { + switch { + case '0' <= c && c <= '9': + return c - '0' + case 'a' <= c && c <= 'f': + return c - 'a' + 10 + case 'A' <= c && c <= 'F': + return c - 'A' + 10 + } + return 0 +} + +func (r *Runtime) builtin_decodeURI(call FunctionCall) Value { + uriString := call.Argument(0).toString() + return r._decode(uriString, &uriReservedHash) +} + +func (r *Runtime) builtin_decodeURIComponent(call FunctionCall) Value { + uriString := call.Argument(0).toString() + return r._decode(uriString, &emptyEscapeSet) +} + +func (r *Runtime) builtin_encodeURI(call FunctionCall) Value { + uriString := call.Argument(0).toString() + return r._encode(uriString, &uriReservedUnescapedHash) +} + +func (r *Runtime) builtin_encodeURIComponent(call FunctionCall) Value { + uriString := call.Argument(0).toString() + return r._encode(uriString, &uriUnescaped) +} + +func (r *Runtime) builtin_escape(call FunctionCall) Value { + s := call.Argument(0).toString() + var sb strings.Builder + l := s.Length() + for i := 0; i < l; i++ { + r := s.CharAt(i) + if r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || + r == '@' || r == '*' || r == '_' || r == '+' || r == '-' || r == '.' || r == '/' { + sb.WriteByte(byte(r)) + } else if r <= 0xff { + sb.WriteByte('%') + sb.WriteByte(hexUpper[r>>4]) + sb.WriteByte(hexUpper[r&0xf]) + } else { + sb.WriteString("%u") + sb.WriteByte(hexUpper[r>>12]) + sb.WriteByte(hexUpper[(r>>8)&0xf]) + sb.WriteByte(hexUpper[(r>>4)&0xf]) + sb.WriteByte(hexUpper[r&0xf]) + } + } + return asciiString(sb.String()) +} + +func (r *Runtime) builtin_unescape(call FunctionCall) Value { + s := call.Argument(0).toString() + l := s.Length() + var asciiBuf []byte + var unicodeBuf []uint16 + _, u := devirtualizeString(s) + unicode := u != nil + if unicode { + unicodeBuf = make([]uint16, 1, l+1) + unicodeBuf[0] = unistring.BOM + } else { + asciiBuf = make([]byte, 0, l) + } + for i := 0; i < l; { + r := s.CharAt(i) + if r == '%' { + if i <= l-6 && s.CharAt(i+1) == 'u' { + c0 := s.CharAt(i + 2) + c1 := s.CharAt(i + 3) + c2 := s.CharAt(i + 4) + c3 := s.CharAt(i + 5) + if c0 <= 0xff && ishex(byte(c0)) && + c1 <= 0xff && ishex(byte(c1)) && + c2 <= 0xff && ishex(byte(c2)) && + c3 <= 0xff && ishex(byte(c3)) { + r = uint16(unhex(byte(c0)))<<12 | + uint16(unhex(byte(c1)))<<8 | + uint16(unhex(byte(c2)))<<4 | + uint16(unhex(byte(c3))) + i += 5 + goto out + } + } + if i <= l-3 { + c0 := s.CharAt(i + 1) + c1 := s.CharAt(i + 2) + if c0 <= 0xff && ishex(byte(c0)) && + c1 <= 0xff && ishex(byte(c1)) { + r = uint16(unhex(byte(c0))<<4 | unhex(byte(c1))) + i += 2 + } + } + } + out: + if r >= utf8.RuneSelf && !unicode { + unicodeBuf = make([]uint16, 1, l+1) + unicodeBuf[0] = unistring.BOM + for _, b := range asciiBuf { + unicodeBuf = append(unicodeBuf, uint16(b)) + } + asciiBuf = nil + unicode = true + } + if unicode { + unicodeBuf = append(unicodeBuf, r) + } else { + asciiBuf = append(asciiBuf, byte(r)) + } + i++ + } + if unicode { + return unicodeString(unicodeBuf) + } + + return asciiString(asciiBuf) +} + +func createGlobalObjectTemplate() *objectTemplate { + t := newObjectTemplate() + t.protoFactory = func(r *Runtime) *Object { + return r.global.ObjectPrototype + } + + t.putStr("Object", func(r *Runtime) Value { return valueProp(r.getObject(), true, false, true) }) + t.putStr("Function", func(r *Runtime) Value { return valueProp(r.getFunction(), true, false, true) }) + t.putStr("Array", func(r *Runtime) Value { return valueProp(r.getArray(), true, false, true) }) + t.putStr("String", func(r *Runtime) Value { return valueProp(r.getString(), true, false, true) }) + t.putStr("Number", func(r *Runtime) Value { return valueProp(r.getNumber(), true, false, true) }) + t.putStr("BigInt", func(r *Runtime) Value { return valueProp(r.getBigInt(), true, false, true) }) + t.putStr("RegExp", func(r *Runtime) Value { return valueProp(r.getRegExp(), true, false, true) }) + t.putStr("Date", func(r *Runtime) Value { return valueProp(r.getDate(), true, false, true) }) + t.putStr("Boolean", func(r *Runtime) Value { return valueProp(r.getBoolean(), true, false, true) }) + t.putStr("Proxy", func(r *Runtime) Value { return valueProp(r.getProxy(), true, false, true) }) + t.putStr("Reflect", func(r *Runtime) Value { return valueProp(r.getReflect(), true, false, true) }) + t.putStr("Error", func(r *Runtime) Value { return valueProp(r.getError(), true, false, true) }) + t.putStr("AggregateError", func(r *Runtime) Value { return valueProp(r.getAggregateError(), true, false, true) }) + t.putStr("TypeError", func(r *Runtime) Value { return valueProp(r.getTypeError(), true, false, true) }) + t.putStr("ReferenceError", func(r *Runtime) Value { return valueProp(r.getReferenceError(), true, false, true) }) + t.putStr("SyntaxError", func(r *Runtime) Value { return valueProp(r.getSyntaxError(), true, false, true) }) + t.putStr("RangeError", func(r *Runtime) Value { return valueProp(r.getRangeError(), true, false, true) }) + t.putStr("EvalError", func(r *Runtime) Value { return valueProp(r.getEvalError(), true, false, true) }) + t.putStr("URIError", func(r *Runtime) Value { return valueProp(r.getURIError(), true, false, true) }) + t.putStr("GoError", func(r *Runtime) Value { return valueProp(r.getGoError(), true, false, true) }) + + t.putStr("eval", func(r *Runtime) Value { return valueProp(r.getEval(), true, false, true) }) + + t.putStr("Math", func(r *Runtime) Value { return valueProp(r.getMath(), true, false, true) }) + t.putStr("JSON", func(r *Runtime) Value { return valueProp(r.getJSON(), true, false, true) }) + addTypedArrays(t) + t.putStr("Symbol", func(r *Runtime) Value { return valueProp(r.getSymbol(), true, false, true) }) + t.putStr("WeakSet", func(r *Runtime) Value { return valueProp(r.getWeakSet(), true, false, true) }) + t.putStr("WeakMap", func(r *Runtime) Value { return valueProp(r.getWeakMap(), true, false, true) }) + t.putStr("Map", func(r *Runtime) Value { return valueProp(r.getMap(), true, false, true) }) + t.putStr("Set", func(r *Runtime) Value { return valueProp(r.getSet(), true, false, true) }) + t.putStr("Promise", func(r *Runtime) Value { return valueProp(r.getPromise(), true, false, true) }) + + t.putStr("globalThis", func(r *Runtime) Value { return valueProp(r.globalObject, true, false, true) }) + t.putStr("NaN", func(r *Runtime) Value { return valueProp(_NaN, false, false, false) }) + t.putStr("undefined", func(r *Runtime) Value { return valueProp(_undefined, false, false, false) }) + t.putStr("Infinity", func(r *Runtime) Value { return valueProp(_positiveInf, false, false, false) }) + + t.putStr("isNaN", func(r *Runtime) Value { return r.methodProp(r.builtin_isNaN, "isNaN", 1) }) + t.putStr("parseInt", func(r *Runtime) Value { return valueProp(r.getParseInt(), true, false, true) }) + t.putStr("parseFloat", func(r *Runtime) Value { return valueProp(r.getParseFloat(), true, false, true) }) + t.putStr("isFinite", func(r *Runtime) Value { return r.methodProp(r.builtin_isFinite, "isFinite", 1) }) + t.putStr("decodeURI", func(r *Runtime) Value { return r.methodProp(r.builtin_decodeURI, "decodeURI", 1) }) + t.putStr("decodeURIComponent", func(r *Runtime) Value { return r.methodProp(r.builtin_decodeURIComponent, "decodeURIComponent", 1) }) + t.putStr("encodeURI", func(r *Runtime) Value { return r.methodProp(r.builtin_encodeURI, "encodeURI", 1) }) + t.putStr("encodeURIComponent", func(r *Runtime) Value { return r.methodProp(r.builtin_encodeURIComponent, "encodeURIComponent", 1) }) + t.putStr("escape", func(r *Runtime) Value { return r.methodProp(r.builtin_escape, "escape", 1) }) + t.putStr("unescape", func(r *Runtime) Value { return r.methodProp(r.builtin_unescape, "unescape", 1) }) + + // TODO: Annex B + + t.putSym(SymToStringTag, func(r *Runtime) Value { return valueProp(asciiString(classGlobal), false, false, true) }) + + return t +} + +var globalObjectTemplate *objectTemplate +var globalObjectTemplateOnce sync.Once + +func getGlobalObjectTemplate() *objectTemplate { + globalObjectTemplateOnce.Do(func() { + globalObjectTemplate = createGlobalObjectTemplate() + }) + return globalObjectTemplate +} + +func (r *Runtime) getEval() *Object { + ret := r.global.Eval + if ret == nil { + ret = r.newNativeFunc(r.builtin_eval, "eval", 1) + r.global.Eval = ret + } + return ret +} + +func digitVal(d byte) int { + var v byte + switch { + case '0' <= d && d <= '9': + v = d - '0' + case 'a' <= d && d <= 'z': + v = d - 'a' + 10 + case 'A' <= d && d <= 'Z': + v = d - 'A' + 10 + default: + return 36 + } + return int(v) +} + +// ECMAScript compatible version of strconv.ParseInt +func parseInt(s string, base int) (Value, error) { + var n int64 + var err error + var cutoff, maxVal int64 + var sign bool + i := 0 + + if len(s) < 1 { + err = strconv.ErrSyntax + goto Error + } + + switch s[0] { + case '-': + sign = true + s = s[1:] + case '+': + s = s[1:] + } + + if len(s) < 1 { + err = strconv.ErrSyntax + goto Error + } + + // Look for hex prefix. + if s[0] == '0' && len(s) > 1 && (s[1] == 'x' || s[1] == 'X') { + if base == 0 || base == 16 { + base = 16 + s = s[2:] + } + } + + switch { + case len(s) < 1: + err = strconv.ErrSyntax + goto Error + + case 2 <= base && base <= 36: + // valid base; nothing to do + + case base == 0: + // Look for hex prefix. + switch { + case s[0] == '0' && len(s) > 1 && (s[1] == 'x' || s[1] == 'X'): + if len(s) < 3 { + err = strconv.ErrSyntax + goto Error + } + base = 16 + s = s[2:] + default: + base = 10 + } + + default: + err = errors.New("invalid base " + strconv.Itoa(base)) + goto Error + } + + // Cutoff is the smallest number such that cutoff*base > maxInt64. + // Use compile-time constants for common cases. + switch base { + case 10: + cutoff = math.MaxInt64/10 + 1 + case 16: + cutoff = math.MaxInt64/16 + 1 + default: + cutoff = math.MaxInt64/int64(base) + 1 + } + + maxVal = math.MaxInt64 + for ; i < len(s); i++ { + if n >= cutoff { + // n*base overflows + return parseLargeInt(float64(n), s[i:], base, sign) + } + v := digitVal(s[i]) + if v >= base { + break + } + n *= int64(base) + + n1 := n + int64(v) + if n1 < n || n1 > maxVal { + // n+v overflows + return parseLargeInt(float64(n)+float64(v), s[i+1:], base, sign) + } + n = n1 + } + + if i == 0 { + err = strconv.ErrSyntax + goto Error + } + + if sign { + n = -n + } + return intToValue(n), nil + +Error: + return _NaN, err +} + +func parseLargeInt(n float64, s string, base int, sign bool) (Value, error) { + i := 0 + b := float64(base) + for ; i < len(s); i++ { + v := digitVal(s[i]) + if v >= base { + break + } + n = n*b + float64(v) + } + if sign { + n = -n + } + // We know it can't be represented as int, so use valueFloat instead of floatToValue + return valueFloat(n), nil +} + +var ( + uriUnescaped [256]bool + uriReserved [256]bool + uriReservedHash [256]bool + uriReservedUnescapedHash [256]bool + emptyEscapeSet [256]bool +) + +func init() { + for _, c := range "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.!~*'()" { + uriUnescaped[c] = true + } + + for _, c := range ";/?:@&=+$," { + uriReserved[c] = true + } + + for i := 0; i < 256; i++ { + if uriUnescaped[i] || uriReserved[i] { + uriReservedUnescapedHash[i] = true + } + uriReservedHash[i] = uriReserved[i] + } + uriReservedUnescapedHash['#'] = true + uriReservedHash['#'] = true +} diff --git a/goja/builtin_global_test.go b/goja/builtin_global_test.go new file mode 100644 index 0000000..5ac8d7f --- /dev/null +++ b/goja/builtin_global_test.go @@ -0,0 +1,21 @@ +package goja + +import ( + "testing" +) + +func TestEncodeURI(t *testing.T) { + const SCRIPT = ` + encodeURI('тест') + ` + + testScript(SCRIPT, asciiString("%D1%82%D0%B5%D1%81%D1%82"), t) +} + +func TestDecodeURI(t *testing.T) { + const SCRIPT = ` + decodeURI("http://ru.wikipedia.org/wiki/%d0%ae%D0%bd%D0%B8%D0%BA%D0%BE%D0%B4") + ` + + testScript(SCRIPT, newStringValue("http://ru.wikipedia.org/wiki/Юникод"), t) +} diff --git a/goja/builtin_json.go b/goja/builtin_json.go new file mode 100644 index 0000000..cd4a7bc --- /dev/null +++ b/goja/builtin_json.go @@ -0,0 +1,542 @@ +package goja + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "math" + "strconv" + "strings" + "unicode/utf16" + "unicode/utf8" + + "github.com/dop251/goja/unistring" +) + +const hex = "0123456789abcdef" + +func (r *Runtime) builtinJSON_parse(call FunctionCall) Value { + d := json.NewDecoder(strings.NewReader(call.Argument(0).toString().String())) + + value, err := r.builtinJSON_decodeValue(d) + if errors.Is(err, io.EOF) { + panic(r.newError(r.getSyntaxError(), "Unexpected end of JSON input (%v)", err.Error())) + } + if err != nil { + panic(r.newError(r.getSyntaxError(), err.Error())) + } + + if tok, err := d.Token(); err != io.EOF { + panic(r.newError(r.getSyntaxError(), "Unexpected token at the end: %v", tok)) + } + + var reviver func(FunctionCall) Value + + if arg1 := call.Argument(1); arg1 != _undefined { + reviver, _ = arg1.ToObject(r).self.assertCallable() + } + + if reviver != nil { + root := r.NewObject() + createDataPropertyOrThrow(root, stringEmpty, value) + return r.builtinJSON_reviveWalk(reviver, root, stringEmpty) + } + + return value +} + +func (r *Runtime) builtinJSON_decodeToken(d *json.Decoder, tok json.Token) (Value, error) { + switch tok := tok.(type) { + case json.Delim: + switch tok { + case '{': + return r.builtinJSON_decodeObject(d) + case '[': + return r.builtinJSON_decodeArray(d) + } + case nil: + return _null, nil + case string: + return newStringValue(tok), nil + case float64: + return floatToValue(tok), nil + case bool: + if tok { + return valueTrue, nil + } + return valueFalse, nil + } + return nil, fmt.Errorf("Unexpected token (%T): %v", tok, tok) +} + +func (r *Runtime) builtinJSON_decodeValue(d *json.Decoder) (Value, error) { + tok, err := d.Token() + if err != nil { + return nil, err + } + return r.builtinJSON_decodeToken(d, tok) +} + +func (r *Runtime) builtinJSON_decodeObject(d *json.Decoder) (*Object, error) { + object := r.NewObject() + for { + key, end, err := r.builtinJSON_decodeObjectKey(d) + if err != nil { + return nil, err + } + if end { + break + } + value, err := r.builtinJSON_decodeValue(d) + if err != nil { + return nil, err + } + + object.self._putProp(unistring.NewFromString(key), value, true, true, true) + } + return object, nil +} + +func (r *Runtime) builtinJSON_decodeObjectKey(d *json.Decoder) (string, bool, error) { + tok, err := d.Token() + if err != nil { + return "", false, err + } + switch tok := tok.(type) { + case json.Delim: + if tok == '}' { + return "", true, nil + } + case string: + return tok, false, nil + } + + return "", false, fmt.Errorf("Unexpected token (%T): %v", tok, tok) +} + +func (r *Runtime) builtinJSON_decodeArray(d *json.Decoder) (*Object, error) { + var arrayValue []Value + for { + tok, err := d.Token() + if err != nil { + return nil, err + } + if delim, ok := tok.(json.Delim); ok { + if delim == ']' { + break + } + } + value, err := r.builtinJSON_decodeToken(d, tok) + if err != nil { + return nil, err + } + arrayValue = append(arrayValue, value) + } + return r.newArrayValues(arrayValue), nil +} + +func (r *Runtime) builtinJSON_reviveWalk(reviver func(FunctionCall) Value, holder *Object, name Value) Value { + value := nilSafe(holder.get(name, nil)) + + if object, ok := value.(*Object); ok { + if isArray(object) { + length := toLength(object.self.getStr("length", nil)) + for index := int64(0); index < length; index++ { + name := asciiString(strconv.FormatInt(index, 10)) + value := r.builtinJSON_reviveWalk(reviver, object, name) + if value == _undefined { + object.delete(name, false) + } else { + createDataProperty(object, name, value) + } + } + } else { + for _, name := range object.self.stringKeys(false, nil) { + value := r.builtinJSON_reviveWalk(reviver, object, name) + if value == _undefined { + object.self.deleteStr(name.string(), false) + } else { + createDataProperty(object, name, value) + } + } + } + } + return reviver(FunctionCall{ + This: holder, + Arguments: []Value{name, value}, + }) +} + +type _builtinJSON_stringifyContext struct { + r *Runtime + stack []*Object + propertyList []Value + replacerFunction func(FunctionCall) Value + gap, indent string + buf bytes.Buffer + allAscii bool +} + +func (r *Runtime) builtinJSON_stringify(call FunctionCall) Value { + ctx := _builtinJSON_stringifyContext{ + r: r, + allAscii: true, + } + + replacer, _ := call.Argument(1).(*Object) + if replacer != nil { + if isArray(replacer) { + length := toLength(replacer.self.getStr("length", nil)) + seen := map[string]bool{} + propertyList := make([]Value, length) + length = 0 + for index := range propertyList { + var name string + value := replacer.self.getIdx(valueInt(int64(index)), nil) + switch v := value.(type) { + case valueFloat, valueInt, String: + name = value.String() + case *Object: + switch v.self.className() { + case classNumber, classString: + name = value.String() + default: + continue + } + default: + continue + } + if seen[name] { + continue + } + seen[name] = true + propertyList[length] = newStringValue(name) + length += 1 + } + ctx.propertyList = propertyList[0:length] + } else if c, ok := replacer.self.assertCallable(); ok { + ctx.replacerFunction = c + } + } + if spaceValue := call.Argument(2); spaceValue != _undefined { + if o, ok := spaceValue.(*Object); ok { + switch oImpl := o.self.(type) { + case *primitiveValueObject: + switch oImpl.pValue.(type) { + case valueInt, valueFloat: + spaceValue = o.ToNumber() + } + case *stringObject: + spaceValue = o.ToString() + } + } + isNum := false + var num int64 + if i, ok := spaceValue.(valueInt); ok { + num = int64(i) + isNum = true + } else if f, ok := spaceValue.(valueFloat); ok { + num = int64(f) + isNum = true + } + if isNum { + if num > 0 { + if num > 10 { + num = 10 + } + ctx.gap = strings.Repeat(" ", int(num)) + } + } else { + if s, ok := spaceValue.(String); ok { + str := s.String() + if len(str) > 10 { + ctx.gap = str[:10] + } else { + ctx.gap = str + } + } + } + } + + if ctx.do(call.Argument(0)) { + if ctx.allAscii { + return asciiString(ctx.buf.String()) + } else { + return &importedString{ + s: ctx.buf.String(), + } + } + } + return _undefined +} + +func (ctx *_builtinJSON_stringifyContext) do(v Value) bool { + holder := ctx.r.NewObject() + createDataPropertyOrThrow(holder, stringEmpty, v) + return ctx.str(stringEmpty, holder) +} + +func (ctx *_builtinJSON_stringifyContext) str(key Value, holder *Object) bool { + value := nilSafe(holder.get(key, nil)) + + switch value.(type) { + case *Object, *valueBigInt: + if toJSON, ok := ctx.r.getVStr(value, "toJSON").(*Object); ok { + if c, ok := toJSON.self.assertCallable(); ok { + value = c(FunctionCall{ + This: value, + Arguments: []Value{key}, + }) + } + } + } + + if ctx.replacerFunction != nil { + value = ctx.replacerFunction(FunctionCall{ + This: holder, + Arguments: []Value{key, value}, + }) + } + + if o, ok := value.(*Object); ok { + switch o1 := o.self.(type) { + case *primitiveValueObject: + switch pValue := o1.pValue.(type) { + case valueInt, valueFloat: + value = o.ToNumber() + default: + value = pValue + } + case *stringObject: + value = o.toString() + case *objectGoReflect: + if o1.toJson != nil { + value = ctx.r.ToValue(o1.toJson()) + } else if v, ok := o1.origValue.Interface().(json.Marshaler); ok { + b, err := v.MarshalJSON() + if err != nil { + panic(ctx.r.NewGoError(err)) + } + ctx.buf.Write(b) + ctx.allAscii = false + return true + } else { + switch o1.className() { + case classNumber: + value = o1.val.ordinaryToPrimitiveNumber() + case classString: + value = o1.val.ordinaryToPrimitiveString() + case classBoolean: + if o.ToInteger() != 0 { + value = valueTrue + } else { + value = valueFalse + } + } + if o1.exportType() == typeBigInt { + value = o1.val.ordinaryToPrimitiveNumber() + } + } + } + } + + switch value1 := value.(type) { + case valueBool: + if value1 { + ctx.buf.WriteString("true") + } else { + ctx.buf.WriteString("false") + } + case String: + ctx.quote(value1) + case valueInt: + ctx.buf.WriteString(value.String()) + case valueFloat: + if !math.IsNaN(float64(value1)) && !math.IsInf(float64(value1), 0) { + ctx.buf.WriteString(value.String()) + } else { + ctx.buf.WriteString("null") + } + case valueNull: + ctx.buf.WriteString("null") + case *valueBigInt: + ctx.r.typeErrorResult(true, "Do not know how to serialize a BigInt") + case *Object: + for _, object := range ctx.stack { + if value1.SameAs(object) { + ctx.r.typeErrorResult(true, "Converting circular structure to JSON") + } + } + ctx.stack = append(ctx.stack, value1) + defer func() { ctx.stack = ctx.stack[:len(ctx.stack)-1] }() + if _, ok := value1.self.assertCallable(); !ok { + if isArray(value1) { + ctx.ja(value1) + } else { + ctx.jo(value1) + } + } else { + return false + } + default: + return false + } + return true +} + +func (ctx *_builtinJSON_stringifyContext) ja(array *Object) { + var stepback string + if ctx.gap != "" { + stepback = ctx.indent + ctx.indent += ctx.gap + } + length := toLength(array.self.getStr("length", nil)) + if length == 0 { + ctx.buf.WriteString("[]") + return + } + + ctx.buf.WriteByte('[') + var separator string + if ctx.gap != "" { + ctx.buf.WriteByte('\n') + ctx.buf.WriteString(ctx.indent) + separator = ",\n" + ctx.indent + } else { + separator = "," + } + + for i := int64(0); i < length; i++ { + if !ctx.str(asciiString(strconv.FormatInt(i, 10)), array) { + ctx.buf.WriteString("null") + } + if i < length-1 { + ctx.buf.WriteString(separator) + } + } + if ctx.gap != "" { + ctx.buf.WriteByte('\n') + ctx.buf.WriteString(stepback) + ctx.indent = stepback + } + ctx.buf.WriteByte(']') +} + +func (ctx *_builtinJSON_stringifyContext) jo(object *Object) { + var stepback string + if ctx.gap != "" { + stepback = ctx.indent + ctx.indent += ctx.gap + } + + ctx.buf.WriteByte('{') + mark := ctx.buf.Len() + var separator string + if ctx.gap != "" { + ctx.buf.WriteByte('\n') + ctx.buf.WriteString(ctx.indent) + separator = ",\n" + ctx.indent + } else { + separator = "," + } + + var props []Value + if ctx.propertyList == nil { + props = object.self.stringKeys(false, nil) + } else { + props = ctx.propertyList + } + + empty := true + for _, name := range props { + off := ctx.buf.Len() + if !empty { + ctx.buf.WriteString(separator) + } + ctx.quote(name.toString()) + if ctx.gap != "" { + ctx.buf.WriteString(": ") + } else { + ctx.buf.WriteByte(':') + } + if ctx.str(name, object) { + if empty { + empty = false + } + } else { + ctx.buf.Truncate(off) + } + } + + if empty { + ctx.buf.Truncate(mark) + } else { + if ctx.gap != "" { + ctx.buf.WriteByte('\n') + ctx.buf.WriteString(stepback) + ctx.indent = stepback + } + } + ctx.buf.WriteByte('}') +} + +func (ctx *_builtinJSON_stringifyContext) quote(str String) { + ctx.buf.WriteByte('"') + reader := &lenientUtf16Decoder{utf16Reader: str.utf16Reader()} + for { + r, _, err := reader.ReadRune() + if err != nil { + break + } + switch r { + case '"', '\\': + ctx.buf.WriteByte('\\') + ctx.buf.WriteByte(byte(r)) + case 0x08: + ctx.buf.WriteString(`\b`) + case 0x09: + ctx.buf.WriteString(`\t`) + case 0x0A: + ctx.buf.WriteString(`\n`) + case 0x0C: + ctx.buf.WriteString(`\f`) + case 0x0D: + ctx.buf.WriteString(`\r`) + default: + if r < 0x20 { + ctx.buf.WriteString(`\u00`) + ctx.buf.WriteByte(hex[r>>4]) + ctx.buf.WriteByte(hex[r&0xF]) + } else { + if utf16.IsSurrogate(r) { + ctx.buf.WriteString(`\u`) + ctx.buf.WriteByte(hex[r>>12]) + ctx.buf.WriteByte(hex[(r>>8)&0xF]) + ctx.buf.WriteByte(hex[(r>>4)&0xF]) + ctx.buf.WriteByte(hex[r&0xF]) + } else { + ctx.buf.WriteRune(r) + if ctx.allAscii && r >= utf8.RuneSelf { + ctx.allAscii = false + } + } + } + } + } + ctx.buf.WriteByte('"') +} + +func (r *Runtime) getJSON() *Object { + ret := r.global.JSON + if ret == nil { + JSON := r.newBaseObject(r.global.ObjectPrototype, classObject) + ret = JSON.val + r.global.JSON = ret + JSON._putProp("parse", r.newNativeFunc(r.builtinJSON_parse, "parse", 2), true, false, true) + JSON._putProp("stringify", r.newNativeFunc(r.builtinJSON_stringify, "stringify", 3), true, false, true) + JSON._putSym(SymToStringTag, valueProp(asciiString(classJSON), false, false, true)) + } + return ret +} diff --git a/goja/builtin_json_test.go b/goja/builtin_json_test.go new file mode 100644 index 0000000..a71c54e --- /dev/null +++ b/goja/builtin_json_test.go @@ -0,0 +1,140 @@ +package goja + +import ( + "encoding/json" + "errors" + "strings" + "testing" + "time" +) + +func TestJSONMarshalObject(t *testing.T) { + vm := New() + o := vm.NewObject() + o.Set("test", 42) + o.Set("testfunc", vm.Get("Error")) + b, err := json.Marshal(o) + if err != nil { + t.Fatal(err) + } + if string(b) != `{"test":42}` { + t.Fatalf("Unexpected value: %s", b) + } +} + +func TestJSONMarshalGoDate(t *testing.T) { + vm := New() + o := vm.NewObject() + o.Set("test", time.Unix(86400, 0).UTC()) + b, err := json.Marshal(o) + if err != nil { + t.Fatal(err) + } + if string(b) != `{"test":"1970-01-02T00:00:00Z"}` { + t.Fatalf("Unexpected value: %s", b) + } +} + +func TestJSONMarshalObjectCircular(t *testing.T) { + vm := New() + o := vm.NewObject() + o.Set("o", o) + _, err := json.Marshal(o) + if err == nil { + t.Fatal("Expected error") + } + if !strings.HasSuffix(err.Error(), "Converting circular structure to JSON") { + t.Fatalf("Unexpected error: %v", err) + } +} + +func TestJSONStringifyCircularWrappedGo(t *testing.T) { + type CircularType struct { + Self *CircularType + } + vm := New() + v := CircularType{} + v.Self = &v + vm.Set("v", &v) + _, err := vm.RunString("JSON.stringify(v)") + if err == nil { + t.Fatal("Expected error") + } + if !strings.HasPrefix(err.Error(), "TypeError: Converting circular structure to JSON") { + t.Fatalf("Unexpected error: %v", err) + } +} + +func TestJSONParseReviver(t *testing.T) { + // example from + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse + const SCRIPT = ` + JSON.parse('{"p": 5}', function(key, value) { + return typeof value === 'number' + ? value * 2 // return value * 2 for numbers + : value // return everything else unchanged + })["p"] + ` + + testScript(SCRIPT, intToValue(10), t) +} + +func TestQuoteMalformedSurrogatePair(t *testing.T) { + testScript(`JSON.stringify("\uD800")`, asciiString(`"\ud800"`), t) +} + +func TestEOFWrapping(t *testing.T) { + vm := New() + + _, err := vm.RunString("JSON.parse('{')") + if err == nil { + t.Fatal("Expected error") + } + + if !strings.Contains(err.Error(), "Unexpected end of JSON input") { + t.Fatalf("Error doesn't contain human-friendly wrapper: %v", err) + } +} + +type testMarshalJSONErrorStruct struct { + e error +} + +func (s *testMarshalJSONErrorStruct) MarshalJSON() ([]byte, error) { + return nil, s.e +} + +func TestMarshalJSONError(t *testing.T) { + vm := New() + v := testMarshalJSONErrorStruct{e: errors.New("test error")} + vm.Set("v", &v) + _, err := vm.RunString("JSON.stringify(v)") + if !errors.Is(err, v.e) { + t.Fatalf("Unexpected error: %v", err) + } +} + +func BenchmarkJSONStringify(b *testing.B) { + b.StopTimer() + vm := New() + var createObj func(level int) *Object + createObj = func(level int) *Object { + o := vm.NewObject() + o.Set("field1", "test") + o.Set("field2", 42) + if level > 0 { + level-- + o.Set("obj1", createObj(level)) + o.Set("obj2", createObj(level)) + } + return o + } + + o := createObj(3) + json := vm.Get("JSON").(*Object) + stringify, _ := AssertFunction(json.Get("stringify")) + b.StartTimer() + for i := 0; i < b.N; i++ { + stringify(nil, o) + } +} diff --git a/goja/builtin_map.go b/goja/builtin_map.go new file mode 100644 index 0000000..819d025 --- /dev/null +++ b/goja/builtin_map.go @@ -0,0 +1,342 @@ +package goja + +import ( + "reflect" +) + +var mapExportType = reflect.TypeOf([][2]interface{}{}) + +type mapObject struct { + baseObject + m *orderedMap +} + +type mapIterObject struct { + baseObject + iter *orderedMapIter + kind iterationKind +} + +func (o *mapIterObject) next() Value { + if o.iter == nil { + return o.val.runtime.createIterResultObject(_undefined, true) + } + + entry := o.iter.next() + if entry == nil { + o.iter = nil + return o.val.runtime.createIterResultObject(_undefined, true) + } + + var result Value + switch o.kind { + case iterationKindKey: + result = entry.key + case iterationKindValue: + result = entry.value + default: + result = o.val.runtime.newArrayValues([]Value{entry.key, entry.value}) + } + + return o.val.runtime.createIterResultObject(result, false) +} + +func (mo *mapObject) init() { + mo.baseObject.init() + mo.m = newOrderedMap(mo.val.runtime.getHash()) +} + +func (mo *mapObject) exportType() reflect.Type { + return mapExportType +} + +func (mo *mapObject) export(ctx *objectExportCtx) interface{} { + m := make([][2]interface{}, mo.m.size) + ctx.put(mo.val, m) + + iter := mo.m.newIter() + for i := 0; i < len(m); i++ { + entry := iter.next() + if entry == nil { + break + } + m[i][0] = exportValue(entry.key, ctx) + m[i][1] = exportValue(entry.value, ctx) + } + + return m +} + +func (mo *mapObject) exportToMap(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + dst.Set(reflect.MakeMap(typ)) + ctx.putTyped(mo.val, typ, dst.Interface()) + keyTyp := typ.Key() + elemTyp := typ.Elem() + iter := mo.m.newIter() + r := mo.val.runtime + for { + entry := iter.next() + if entry == nil { + break + } + keyVal := reflect.New(keyTyp).Elem() + err := r.toReflectValue(entry.key, keyVal, ctx) + if err != nil { + return err + } + elemVal := reflect.New(elemTyp).Elem() + err = r.toReflectValue(entry.value, elemVal, ctx) + if err != nil { + return err + } + dst.SetMapIndex(keyVal, elemVal) + } + return nil +} + +func (r *Runtime) mapProto_clear(call FunctionCall) Value { + thisObj := r.toObject(call.This) + mo, ok := thisObj.self.(*mapObject) + if !ok { + panic(r.NewTypeError("Method Map.prototype.clear called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + + mo.m.clear() + + return _undefined +} + +func (r *Runtime) mapProto_delete(call FunctionCall) Value { + thisObj := r.toObject(call.This) + mo, ok := thisObj.self.(*mapObject) + if !ok { + panic(r.NewTypeError("Method Map.prototype.delete called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + + return r.toBoolean(mo.m.remove(call.Argument(0))) +} + +func (r *Runtime) mapProto_get(call FunctionCall) Value { + thisObj := r.toObject(call.This) + mo, ok := thisObj.self.(*mapObject) + if !ok { + panic(r.NewTypeError("Method Map.prototype.get called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + + return nilSafe(mo.m.get(call.Argument(0))) +} + +func (r *Runtime) mapProto_has(call FunctionCall) Value { + thisObj := r.toObject(call.This) + mo, ok := thisObj.self.(*mapObject) + if !ok { + panic(r.NewTypeError("Method Map.prototype.has called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + if mo.m.has(call.Argument(0)) { + return valueTrue + } + return valueFalse +} + +func (r *Runtime) mapProto_set(call FunctionCall) Value { + thisObj := r.toObject(call.This) + mo, ok := thisObj.self.(*mapObject) + if !ok { + panic(r.NewTypeError("Method Map.prototype.set called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + mo.m.set(call.Argument(0), call.Argument(1)) + return call.This +} + +func (r *Runtime) mapProto_entries(call FunctionCall) Value { + return r.createMapIterator(call.This, iterationKindKeyValue) +} + +func (r *Runtime) mapProto_forEach(call FunctionCall) Value { + thisObj := r.toObject(call.This) + mo, ok := thisObj.self.(*mapObject) + if !ok { + panic(r.NewTypeError("Method Map.prototype.forEach called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + callbackFn, ok := r.toObject(call.Argument(0)).self.assertCallable() + if !ok { + panic(r.NewTypeError("object is not a function %s")) + } + t := call.Argument(1) + iter := mo.m.newIter() + for { + entry := iter.next() + if entry == nil { + break + } + callbackFn(FunctionCall{This: t, Arguments: []Value{entry.value, entry.key, thisObj}}) + } + + return _undefined +} + +func (r *Runtime) mapProto_keys(call FunctionCall) Value { + return r.createMapIterator(call.This, iterationKindKey) +} + +func (r *Runtime) mapProto_values(call FunctionCall) Value { + return r.createMapIterator(call.This, iterationKindValue) +} + +func (r *Runtime) mapProto_getSize(call FunctionCall) Value { + thisObj := r.toObject(call.This) + mo, ok := thisObj.self.(*mapObject) + if !ok { + panic(r.NewTypeError("Method get Map.prototype.size called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + return intToValue(int64(mo.m.size)) +} + +func (r *Runtime) builtin_newMap(args []Value, newTarget *Object) *Object { + if newTarget == nil { + panic(r.needNew("Map")) + } + proto := r.getPrototypeFromCtor(newTarget, r.global.Map, r.global.MapPrototype) + o := &Object{runtime: r} + + mo := &mapObject{} + mo.class = classObject + mo.val = o + mo.extensible = true + o.self = mo + mo.prototype = proto + mo.init() + if len(args) > 0 { + if arg := args[0]; arg != nil && arg != _undefined && arg != _null { + adder := mo.getStr("set", nil) + adderFn := toMethod(adder) + if adderFn == nil { + panic(r.NewTypeError("Map.set in missing")) + } + iter := r.getIterator(arg, nil) + i0 := valueInt(0) + i1 := valueInt(1) + if adder == r.global.mapAdder { + iter.iterate(func(item Value) { + itemObj := r.toObject(item) + k := nilSafe(itemObj.self.getIdx(i0, nil)) + v := nilSafe(itemObj.self.getIdx(i1, nil)) + mo.m.set(k, v) + }) + } else { + iter.iterate(func(item Value) { + itemObj := r.toObject(item) + k := itemObj.self.getIdx(i0, nil) + v := itemObj.self.getIdx(i1, nil) + adderFn(FunctionCall{This: o, Arguments: []Value{k, v}}) + }) + } + } + } + return o +} + +func (r *Runtime) createMapIterator(mapValue Value, kind iterationKind) Value { + obj := r.toObject(mapValue) + mapObj, ok := obj.self.(*mapObject) + if !ok { + panic(r.NewTypeError("Object is not a Map")) + } + + o := &Object{runtime: r} + + mi := &mapIterObject{ + iter: mapObj.m.newIter(), + kind: kind, + } + mi.class = classObject + mi.val = o + mi.extensible = true + o.self = mi + mi.prototype = r.getMapIteratorPrototype() + mi.init() + + return o +} + +func (r *Runtime) mapIterProto_next(call FunctionCall) Value { + thisObj := r.toObject(call.This) + if iter, ok := thisObj.self.(*mapIterObject); ok { + return iter.next() + } + panic(r.NewTypeError("Method Map Iterator.prototype.next called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) +} + +func (r *Runtime) createMapProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.global.ObjectPrototype, classObject) + + o._putProp("constructor", r.getMap(), true, false, true) + o._putProp("clear", r.newNativeFunc(r.mapProto_clear, "clear", 0), true, false, true) + r.global.mapAdder = r.newNativeFunc(r.mapProto_set, "set", 2) + o._putProp("set", r.global.mapAdder, true, false, true) + o._putProp("delete", r.newNativeFunc(r.mapProto_delete, "delete", 1), true, false, true) + o._putProp("forEach", r.newNativeFunc(r.mapProto_forEach, "forEach", 1), true, false, true) + o._putProp("has", r.newNativeFunc(r.mapProto_has, "has", 1), true, false, true) + o._putProp("get", r.newNativeFunc(r.mapProto_get, "get", 1), true, false, true) + o.setOwnStr("size", &valueProperty{ + getterFunc: r.newNativeFunc(r.mapProto_getSize, "get size", 0), + accessor: true, + writable: true, + configurable: true, + }, true) + o._putProp("keys", r.newNativeFunc(r.mapProto_keys, "keys", 0), true, false, true) + o._putProp("values", r.newNativeFunc(r.mapProto_values, "values", 0), true, false, true) + + entriesFunc := r.newNativeFunc(r.mapProto_entries, "entries", 0) + o._putProp("entries", entriesFunc, true, false, true) + o._putSym(SymIterator, valueProp(entriesFunc, true, false, true)) + o._putSym(SymToStringTag, valueProp(asciiString(classMap), false, false, true)) + + return o +} + +func (r *Runtime) createMap(val *Object) objectImpl { + o := r.newNativeConstructOnly(val, r.builtin_newMap, r.getMapPrototype(), "Map", 0) + r.putSpeciesReturnThis(o) + + return o +} + +func (r *Runtime) createMapIterProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.getIteratorPrototype(), classObject) + + o._putProp("next", r.newNativeFunc(r.mapIterProto_next, "next", 0), true, false, true) + o._putSym(SymToStringTag, valueProp(asciiString(classMapIterator), false, false, true)) + + return o +} + +func (r *Runtime) getMapIteratorPrototype() *Object { + var o *Object + if o = r.global.MapIteratorPrototype; o == nil { + o = &Object{runtime: r} + r.global.MapIteratorPrototype = o + o.self = r.createMapIterProto(o) + } + return o +} + +func (r *Runtime) getMapPrototype() *Object { + ret := r.global.MapPrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.MapPrototype = ret + ret.self = r.createMapProto(ret) + } + return ret +} + +func (r *Runtime) getMap() *Object { + ret := r.global.Map + if ret == nil { + ret = &Object{runtime: r} + r.global.Map = ret + ret.self = r.createMap(ret) + } + return ret +} diff --git a/goja/builtin_map_test.go b/goja/builtin_map_test.go new file mode 100644 index 0000000..e6afafe --- /dev/null +++ b/goja/builtin_map_test.go @@ -0,0 +1,244 @@ +package goja + +import ( + "fmt" + "hash/maphash" + "testing" +) + +func TestMapEvilIterator(t *testing.T) { + const SCRIPT = ` + 'use strict'; + var o = {}; + + function Iter(value) { + this.value = value; + this.idx = 0; + } + + Iter.prototype.next = function() { + var idx = this.idx; + if (idx === 0) { + this.idx++; + return this.value; + } + return {done: true}; + } + + o[Symbol.iterator] = function() { + return new Iter({}); + } + + assert.throws(TypeError, function() { + new Map(o); + }); + + o[Symbol.iterator] = function() { + return new Iter({value: []}); + } + + function t(prefix) { + var m = new Map(o); + assert.sameValue(1, m.size, prefix+": m.size"); + assert.sameValue(true, m.has(undefined), prefix+": m.has(undefined)"); + assert.sameValue(undefined, m.get(undefined), prefix+": m.get(undefined)"); + } + + t("standard adder"); + + var count = 0; + var origSet = Map.prototype.set; + + Map.prototype.set = function() { + count++; + origSet.apply(this, arguments); + } + + t("custom adder"); + assert.sameValue(1, count, "count"); + + undefined; + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestMapExportToNilMap(t *testing.T) { + vm := New() + var m map[int]interface{} + res, err := vm.RunString("new Map([[1, true]])") + if err != nil { + t.Fatal(err) + } + err = vm.ExportTo(res, &m) + if err != nil { + t.Fatal(err) + } + if len(m) != 1 { + t.Fatal(m) + } + if _, exists := m[1]; !exists { + t.Fatal(m) + } +} + +func TestMapExportToNonNilMap(t *testing.T) { + vm := New() + m := map[int]interface{}{ + 2: true, + } + res, err := vm.RunString("new Map([[1, true]])") + if err != nil { + t.Fatal(err) + } + err = vm.ExportTo(res, &m) + if err != nil { + t.Fatal(err) + } + if len(m) != 1 { + t.Fatal(m) + } + if _, exists := m[1]; !exists { + t.Fatal(m) + } +} + +func TestMapGetAdderGetIteratorOrder(t *testing.T) { + const SCRIPT = ` + let getterCalled = 0; + + class M extends Map { + get set() { + getterCalled++; + return null; + } + } + + let getIteratorCalled = 0; + + let iterable = {}; + iterable[Symbol.iterator] = () => { + getIteratorCalled++ + return { + next: 1 + }; + } + + let thrown = false; + + try { + new M(iterable); + } catch (e) { + if (e instanceof TypeError) { + thrown = true; + } else { + throw e; + } + } + + thrown && getterCalled === 1 && getIteratorCalled === 0; + ` + testScript(SCRIPT, valueTrue, t) +} + +func ExampleObject_Export_map() { + vm := New() + m, err := vm.RunString(` + new Map([[1, true], [2, false]]); + `) + if err != nil { + panic(err) + } + exp := m.Export() + fmt.Printf("%T, %v\n", exp, exp) + // Output: [][2]interface {}, [[1 true] [2 false]] +} + +func ExampleRuntime_ExportTo_mapToMap() { + vm := New() + m, err := vm.RunString(` + new Map([[1, true], [2, false]]); + `) + if err != nil { + panic(err) + } + exp := make(map[int]bool) + err = vm.ExportTo(m, &exp) + if err != nil { + panic(err) + } + fmt.Println(exp) + // Output: map[1:true 2:false] +} + +func ExampleRuntime_ExportTo_mapToSlice() { + vm := New() + m, err := vm.RunString(` + new Map([[1, true], [2, false]]); + `) + if err != nil { + panic(err) + } + exp := make([][]interface{}, 0) + err = vm.ExportTo(m, &exp) + if err != nil { + panic(err) + } + fmt.Println(exp) + // Output: [[1 true] [2 false]] +} + +func ExampleRuntime_ExportTo_mapToTypedSlice() { + vm := New() + m, err := vm.RunString(` + new Map([[1, true], [2, false]]); + `) + if err != nil { + panic(err) + } + exp := make([][2]interface{}, 0) + err = vm.ExportTo(m, &exp) + if err != nil { + panic(err) + } + fmt.Println(exp) + // Output: [[1 true] [2 false]] +} + +func BenchmarkMapDelete(b *testing.B) { + var key1 Value = asciiString("a") + var key2 Value = asciiString("b") + one := intToValue(1) + two := intToValue(2) + for i := 0; i < b.N; i++ { + m := newOrderedMap(&maphash.Hash{}) + m.set(key1, one) + m.set(key2, two) + if !m.remove(key1) { + b.Fatal("remove() returned false") + } + } +} + +func BenchmarkMapDeleteJS(b *testing.B) { + prg, err := Compile("test.js", ` + var m = new Map([['a',1], ['b', 2]]); + + var result = m.delete('a'); + + if (!result || m.size !== 1) { + throw new Error("Fail!"); + } + `, + false) + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + vm := New() + _, err := vm.RunProgram(prg) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/goja/builtin_math.go b/goja/builtin_math.go new file mode 100644 index 0000000..169ea18 --- /dev/null +++ b/goja/builtin_math.go @@ -0,0 +1,358 @@ +package goja + +import ( + "math" + "math/bits" + "sync" +) + +func (r *Runtime) math_abs(call FunctionCall) Value { + return floatToValue(math.Abs(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_acos(call FunctionCall) Value { + return floatToValue(math.Acos(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_acosh(call FunctionCall) Value { + return floatToValue(math.Acosh(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_asin(call FunctionCall) Value { + return floatToValue(math.Asin(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_asinh(call FunctionCall) Value { + return floatToValue(math.Asinh(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_atan(call FunctionCall) Value { + return floatToValue(math.Atan(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_atanh(call FunctionCall) Value { + return floatToValue(math.Atanh(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_atan2(call FunctionCall) Value { + y := call.Argument(0).ToFloat() + x := call.Argument(1).ToFloat() + + return floatToValue(math.Atan2(y, x)) +} + +func (r *Runtime) math_cbrt(call FunctionCall) Value { + return floatToValue(math.Cbrt(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_ceil(call FunctionCall) Value { + return floatToValue(math.Ceil(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_clz32(call FunctionCall) Value { + return intToValue(int64(bits.LeadingZeros32(toUint32(call.Argument(0))))) +} + +func (r *Runtime) math_cos(call FunctionCall) Value { + return floatToValue(math.Cos(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_cosh(call FunctionCall) Value { + return floatToValue(math.Cosh(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_exp(call FunctionCall) Value { + return floatToValue(math.Exp(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_expm1(call FunctionCall) Value { + return floatToValue(math.Expm1(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_floor(call FunctionCall) Value { + return floatToValue(math.Floor(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_fround(call FunctionCall) Value { + return floatToValue(float64(float32(call.Argument(0).ToFloat()))) +} + +func (r *Runtime) math_hypot(call FunctionCall) Value { + var max float64 + var hasNaN bool + absValues := make([]float64, 0, len(call.Arguments)) + for _, v := range call.Arguments { + arg := nilSafe(v).ToFloat() + if math.IsNaN(arg) { + hasNaN = true + } else { + abs := math.Abs(arg) + if abs > max { + max = abs + } + absValues = append(absValues, abs) + } + } + if math.IsInf(max, 1) { + return _positiveInf + } + if hasNaN { + return _NaN + } + if max == 0 { + return _positiveZero + } + + // Kahan summation to avoid rounding errors. + // Normalize the numbers to the largest one to avoid overflow. + var sum, compensation float64 + for _, n := range absValues { + n /= max + summand := n*n - compensation + preliminary := sum + summand + compensation = (preliminary - sum) - summand + sum = preliminary + } + return floatToValue(math.Sqrt(sum) * max) +} + +func (r *Runtime) math_imul(call FunctionCall) Value { + x := toUint32(call.Argument(0)) + y := toUint32(call.Argument(1)) + return intToValue(int64(int32(x * y))) +} + +func (r *Runtime) math_log(call FunctionCall) Value { + return floatToValue(math.Log(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_log1p(call FunctionCall) Value { + return floatToValue(math.Log1p(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_log10(call FunctionCall) Value { + return floatToValue(math.Log10(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_log2(call FunctionCall) Value { + return floatToValue(math.Log2(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_max(call FunctionCall) Value { + result := math.Inf(-1) + args := call.Arguments + for i, arg := range args { + n := nilSafe(arg).ToFloat() + if math.IsNaN(n) { + args = args[i+1:] + goto NaNLoop + } + result = math.Max(result, n) + } + + return floatToValue(result) + +NaNLoop: + // All arguments still need to be coerced to number according to the specs. + for _, arg := range args { + nilSafe(arg).ToFloat() + } + return _NaN +} + +func (r *Runtime) math_min(call FunctionCall) Value { + result := math.Inf(1) + args := call.Arguments + for i, arg := range args { + n := nilSafe(arg).ToFloat() + if math.IsNaN(n) { + args = args[i+1:] + goto NaNLoop + } + result = math.Min(result, n) + } + + return floatToValue(result) + +NaNLoop: + // All arguments still need to be coerced to number according to the specs. + for _, arg := range args { + nilSafe(arg).ToFloat() + } + return _NaN +} + +func pow(x, y Value) Value { + if x, ok := x.(valueInt); ok { + if y, ok := y.(valueInt); ok && y >= 0 { + if y == 0 { + return intToValue(1) + } + if x == 0 { + return intToValue(0) + } + ip := ipow(int64(x), int64(y)) + if ip != 0 { + return intToValue(ip) + } + } + } + xf := x.ToFloat() + yf := y.ToFloat() + if math.Abs(xf) == 1 && math.IsInf(yf, 0) { + return _NaN + } + if xf == 1 && math.IsNaN(yf) { + return _NaN + } + return floatToValue(math.Pow(xf, yf)) +} + +func (r *Runtime) math_pow(call FunctionCall) Value { + return pow(call.Argument(0), call.Argument(1)) +} + +func (r *Runtime) math_random(call FunctionCall) Value { + return floatToValue(r.rand()) +} + +func (r *Runtime) math_round(call FunctionCall) Value { + f := call.Argument(0).ToFloat() + if math.IsNaN(f) { + return _NaN + } + + if f == 0 && math.Signbit(f) { + return _negativeZero + } + + t := math.Trunc(f) + + if f >= 0 { + if f-t >= 0.5 { + return floatToValue(t + 1) + } + } else { + if t-f > 0.5 { + return floatToValue(t - 1) + } + } + + return floatToValue(t) +} + +func (r *Runtime) math_sign(call FunctionCall) Value { + arg := call.Argument(0) + num := arg.ToFloat() + if math.IsNaN(num) || num == 0 { // this will match -0 too + return arg + } + if num > 0 { + return intToValue(1) + } + return intToValue(-1) +} + +func (r *Runtime) math_sin(call FunctionCall) Value { + return floatToValue(math.Sin(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_sinh(call FunctionCall) Value { + return floatToValue(math.Sinh(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_sqrt(call FunctionCall) Value { + return floatToValue(math.Sqrt(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_tan(call FunctionCall) Value { + return floatToValue(math.Tan(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_tanh(call FunctionCall) Value { + return floatToValue(math.Tanh(call.Argument(0).ToFloat())) +} + +func (r *Runtime) math_trunc(call FunctionCall) Value { + arg := call.Argument(0) + if i, ok := arg.(valueInt); ok { + return i + } + return floatToValue(math.Trunc(arg.ToFloat())) +} + +func createMathTemplate() *objectTemplate { + t := newObjectTemplate() + t.protoFactory = func(r *Runtime) *Object { + return r.global.ObjectPrototype + } + + t.putStr("E", func(r *Runtime) Value { return valueProp(valueFloat(math.E), false, false, false) }) + t.putStr("LN10", func(r *Runtime) Value { return valueProp(valueFloat(math.Ln10), false, false, false) }) + t.putStr("LN2", func(r *Runtime) Value { return valueProp(valueFloat(math.Ln2), false, false, false) }) + t.putStr("LOG10E", func(r *Runtime) Value { return valueProp(valueFloat(math.Log10E), false, false, false) }) + t.putStr("LOG2E", func(r *Runtime) Value { return valueProp(valueFloat(math.Log2E), false, false, false) }) + t.putStr("PI", func(r *Runtime) Value { return valueProp(valueFloat(math.Pi), false, false, false) }) + t.putStr("SQRT1_2", func(r *Runtime) Value { return valueProp(valueFloat(sqrt1_2), false, false, false) }) + t.putStr("SQRT2", func(r *Runtime) Value { return valueProp(valueFloat(math.Sqrt2), false, false, false) }) + + t.putSym(SymToStringTag, func(r *Runtime) Value { return valueProp(asciiString(classMath), false, false, true) }) + + t.putStr("abs", func(r *Runtime) Value { return r.methodProp(r.math_abs, "abs", 1) }) + t.putStr("acos", func(r *Runtime) Value { return r.methodProp(r.math_acos, "acos", 1) }) + t.putStr("acosh", func(r *Runtime) Value { return r.methodProp(r.math_acosh, "acosh", 1) }) + t.putStr("asin", func(r *Runtime) Value { return r.methodProp(r.math_asin, "asin", 1) }) + t.putStr("asinh", func(r *Runtime) Value { return r.methodProp(r.math_asinh, "asinh", 1) }) + t.putStr("atan", func(r *Runtime) Value { return r.methodProp(r.math_atan, "atan", 1) }) + t.putStr("atanh", func(r *Runtime) Value { return r.methodProp(r.math_atanh, "atanh", 1) }) + t.putStr("atan2", func(r *Runtime) Value { return r.methodProp(r.math_atan2, "atan2", 2) }) + t.putStr("cbrt", func(r *Runtime) Value { return r.methodProp(r.math_cbrt, "cbrt", 1) }) + t.putStr("ceil", func(r *Runtime) Value { return r.methodProp(r.math_ceil, "ceil", 1) }) + t.putStr("clz32", func(r *Runtime) Value { return r.methodProp(r.math_clz32, "clz32", 1) }) + t.putStr("cos", func(r *Runtime) Value { return r.methodProp(r.math_cos, "cos", 1) }) + t.putStr("cosh", func(r *Runtime) Value { return r.methodProp(r.math_cosh, "cosh", 1) }) + t.putStr("exp", func(r *Runtime) Value { return r.methodProp(r.math_exp, "exp", 1) }) + t.putStr("expm1", func(r *Runtime) Value { return r.methodProp(r.math_expm1, "expm1", 1) }) + t.putStr("floor", func(r *Runtime) Value { return r.methodProp(r.math_floor, "floor", 1) }) + t.putStr("fround", func(r *Runtime) Value { return r.methodProp(r.math_fround, "fround", 1) }) + t.putStr("hypot", func(r *Runtime) Value { return r.methodProp(r.math_hypot, "hypot", 2) }) + t.putStr("imul", func(r *Runtime) Value { return r.methodProp(r.math_imul, "imul", 2) }) + t.putStr("log", func(r *Runtime) Value { return r.methodProp(r.math_log, "log", 1) }) + t.putStr("log1p", func(r *Runtime) Value { return r.methodProp(r.math_log1p, "log1p", 1) }) + t.putStr("log10", func(r *Runtime) Value { return r.methodProp(r.math_log10, "log10", 1) }) + t.putStr("log2", func(r *Runtime) Value { return r.methodProp(r.math_log2, "log2", 1) }) + t.putStr("max", func(r *Runtime) Value { return r.methodProp(r.math_max, "max", 2) }) + t.putStr("min", func(r *Runtime) Value { return r.methodProp(r.math_min, "min", 2) }) + t.putStr("pow", func(r *Runtime) Value { return r.methodProp(r.math_pow, "pow", 2) }) + t.putStr("random", func(r *Runtime) Value { return r.methodProp(r.math_random, "random", 0) }) + t.putStr("round", func(r *Runtime) Value { return r.methodProp(r.math_round, "round", 1) }) + t.putStr("sign", func(r *Runtime) Value { return r.methodProp(r.math_sign, "sign", 1) }) + t.putStr("sin", func(r *Runtime) Value { return r.methodProp(r.math_sin, "sin", 1) }) + t.putStr("sinh", func(r *Runtime) Value { return r.methodProp(r.math_sinh, "sinh", 1) }) + t.putStr("sqrt", func(r *Runtime) Value { return r.methodProp(r.math_sqrt, "sqrt", 1) }) + t.putStr("tan", func(r *Runtime) Value { return r.methodProp(r.math_tan, "tan", 1) }) + t.putStr("tanh", func(r *Runtime) Value { return r.methodProp(r.math_tanh, "tanh", 1) }) + t.putStr("trunc", func(r *Runtime) Value { return r.methodProp(r.math_trunc, "trunc", 1) }) + + return t +} + +var mathTemplate *objectTemplate +var mathTemplateOnce sync.Once + +func getMathTemplate() *objectTemplate { + mathTemplateOnce.Do(func() { + mathTemplate = createMathTemplate() + }) + return mathTemplate +} + +func (r *Runtime) getMath() *Object { + ret := r.global.Math + if ret == nil { + ret = &Object{runtime: r} + r.global.Math = ret + r.newTemplatedObject(getMathTemplate(), ret) + } + return ret +} diff --git a/goja/builtin_number.go b/goja/builtin_number.go new file mode 100644 index 0000000..43add4f --- /dev/null +++ b/goja/builtin_number.go @@ -0,0 +1,303 @@ +package goja + +import ( + "math" + "sync" + + "github.com/dop251/goja/ftoa" +) + +func (r *Runtime) toNumber(v Value) Value { + switch t := v.(type) { + case valueFloat, valueInt: + return v + case *Object: + switch t := t.self.(type) { + case *primitiveValueObject: + return r.toNumber(t.pValue) + case *objectGoReflect: + if t.class == classNumber && t.valueOf != nil { + return t.valueOf() + } + } + if t == r.global.NumberPrototype { + return _positiveZero + } + } + panic(r.NewTypeError("Value is not a number: %s", v)) +} + +func (r *Runtime) numberproto_valueOf(call FunctionCall) Value { + return r.toNumber(call.This) +} + +func (r *Runtime) numberproto_toString(call FunctionCall) Value { + var numVal Value + switch t := call.This.(type) { + case valueFloat, valueInt: + numVal = t + case *Object: + switch t := t.self.(type) { + case *primitiveValueObject: + numVal = r.toNumber(t.pValue) + case *objectGoReflect: + if t.class == classNumber { + if t.toString != nil { + return t.toString() + } + if t.valueOf != nil { + numVal = t.valueOf() + } + } + } + if t == r.global.NumberPrototype { + return asciiString("0") + } + } + if numVal == nil { + panic(r.NewTypeError("Value is not a number")) + } + var radix int + if arg := call.Argument(0); arg != _undefined { + radix = int(arg.ToInteger()) + } else { + radix = 10 + } + + if radix < 2 || radix > 36 { + panic(r.newError(r.getRangeError(), "toString() radix argument must be between 2 and 36")) + } + + num := numVal.ToFloat() + + if math.IsNaN(num) { + return stringNaN + } + + if math.IsInf(num, 1) { + return stringInfinity + } + + if math.IsInf(num, -1) { + return stringNegInfinity + } + + if radix == 10 { + return asciiString(fToStr(num, ftoa.ModeStandard, 0)) + } + + return asciiString(ftoa.FToBaseStr(num, radix)) +} + +func (r *Runtime) numberproto_toFixed(call FunctionCall) Value { + num := r.toNumber(call.This).ToFloat() + prec := call.Argument(0).ToInteger() + + if prec < 0 || prec > 100 { + panic(r.newError(r.getRangeError(), "toFixed() precision must be between 0 and 100")) + } + if math.IsNaN(num) { + return stringNaN + } + return asciiString(fToStr(num, ftoa.ModeFixed, int(prec))) +} + +func (r *Runtime) numberproto_toExponential(call FunctionCall) Value { + num := r.toNumber(call.This).ToFloat() + precVal := call.Argument(0) + var prec int64 + if precVal == _undefined { + return asciiString(fToStr(num, ftoa.ModeStandardExponential, 0)) + } else { + prec = precVal.ToInteger() + } + + if math.IsNaN(num) { + return stringNaN + } + if math.IsInf(num, 1) { + return stringInfinity + } + if math.IsInf(num, -1) { + return stringNegInfinity + } + + if prec < 0 || prec > 100 { + panic(r.newError(r.getRangeError(), "toExponential() precision must be between 0 and 100")) + } + + return asciiString(fToStr(num, ftoa.ModeExponential, int(prec+1))) +} + +func (r *Runtime) numberproto_toPrecision(call FunctionCall) Value { + numVal := r.toNumber(call.This) + precVal := call.Argument(0) + if precVal == _undefined { + return numVal.toString() + } + num := numVal.ToFloat() + prec := precVal.ToInteger() + + if math.IsNaN(num) { + return stringNaN + } + if math.IsInf(num, 1) { + return stringInfinity + } + if math.IsInf(num, -1) { + return stringNegInfinity + } + if prec < 1 || prec > 100 { + panic(r.newError(r.getRangeError(), "toPrecision() precision must be between 1 and 100")) + } + + return asciiString(fToStr(num, ftoa.ModePrecision, int(prec))) +} + +func (r *Runtime) number_isFinite(call FunctionCall) Value { + switch arg := call.Argument(0).(type) { + case valueInt: + return valueTrue + case valueFloat: + f := float64(arg) + return r.toBoolean(!math.IsInf(f, 0) && !math.IsNaN(f)) + default: + return valueFalse + } +} + +func (r *Runtime) number_isInteger(call FunctionCall) Value { + switch arg := call.Argument(0).(type) { + case valueInt: + return valueTrue + case valueFloat: + f := float64(arg) + return r.toBoolean(!math.IsNaN(f) && !math.IsInf(f, 0) && math.Floor(f) == f) + default: + return valueFalse + } +} + +func (r *Runtime) number_isNaN(call FunctionCall) Value { + if f, ok := call.Argument(0).(valueFloat); ok && math.IsNaN(float64(f)) { + return valueTrue + } + return valueFalse +} + +func (r *Runtime) number_isSafeInteger(call FunctionCall) Value { + arg := call.Argument(0) + if i, ok := arg.(valueInt); ok && i >= -(maxInt-1) && i <= maxInt-1 { + return valueTrue + } + if arg == _negativeZero { + return valueTrue + } + return valueFalse +} + +func createNumberProtoTemplate() *objectTemplate { + t := newObjectTemplate() + t.protoFactory = func(r *Runtime) *Object { + return r.global.ObjectPrototype + } + + t.putStr("constructor", func(r *Runtime) Value { return valueProp(r.getNumber(), true, false, true) }) + + t.putStr("toExponential", func(r *Runtime) Value { return r.methodProp(r.numberproto_toExponential, "toExponential", 1) }) + t.putStr("toFixed", func(r *Runtime) Value { return r.methodProp(r.numberproto_toFixed, "toFixed", 1) }) + t.putStr("toLocaleString", func(r *Runtime) Value { return r.methodProp(r.numberproto_toString, "toLocaleString", 0) }) + t.putStr("toPrecision", func(r *Runtime) Value { return r.methodProp(r.numberproto_toPrecision, "toPrecision", 1) }) + t.putStr("toString", func(r *Runtime) Value { return r.methodProp(r.numberproto_toString, "toString", 1) }) + t.putStr("valueOf", func(r *Runtime) Value { return r.methodProp(r.numberproto_valueOf, "valueOf", 0) }) + + return t +} + +var numberProtoTemplate *objectTemplate +var numberProtoTemplateOnce sync.Once + +func getNumberProtoTemplate() *objectTemplate { + numberProtoTemplateOnce.Do(func() { + numberProtoTemplate = createNumberProtoTemplate() + }) + return numberProtoTemplate +} + +func (r *Runtime) getNumberPrototype() *Object { + ret := r.global.NumberPrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.NumberPrototype = ret + o := r.newTemplatedObject(getNumberProtoTemplate(), ret) + o.class = classNumber + } + return ret +} + +func (r *Runtime) getParseFloat() *Object { + ret := r.global.parseFloat + if ret == nil { + ret = r.newNativeFunc(r.builtin_parseFloat, "parseFloat", 1) + r.global.parseFloat = ret + } + return ret +} + +func (r *Runtime) getParseInt() *Object { + ret := r.global.parseInt + if ret == nil { + ret = r.newNativeFunc(r.builtin_parseInt, "parseInt", 2) + r.global.parseInt = ret + } + return ret +} + +func createNumberTemplate() *objectTemplate { + t := newObjectTemplate() + t.protoFactory = func(r *Runtime) *Object { + return r.getFunctionPrototype() + } + t.putStr("length", func(r *Runtime) Value { return valueProp(intToValue(1), false, false, true) }) + t.putStr("name", func(r *Runtime) Value { return valueProp(asciiString("Number"), false, false, true) }) + + t.putStr("prototype", func(r *Runtime) Value { return valueProp(r.getNumberPrototype(), false, false, false) }) + + t.putStr("EPSILON", func(r *Runtime) Value { return valueProp(_epsilon, false, false, false) }) + t.putStr("isFinite", func(r *Runtime) Value { return r.methodProp(r.number_isFinite, "isFinite", 1) }) + t.putStr("isInteger", func(r *Runtime) Value { return r.methodProp(r.number_isInteger, "isInteger", 1) }) + t.putStr("isNaN", func(r *Runtime) Value { return r.methodProp(r.number_isNaN, "isNaN", 1) }) + t.putStr("isSafeInteger", func(r *Runtime) Value { return r.methodProp(r.number_isSafeInteger, "isSafeInteger", 1) }) + t.putStr("MAX_SAFE_INTEGER", func(r *Runtime) Value { return valueProp(valueInt(maxInt-1), false, false, false) }) + t.putStr("MIN_SAFE_INTEGER", func(r *Runtime) Value { return valueProp(valueInt(-(maxInt - 1)), false, false, false) }) + t.putStr("MIN_VALUE", func(r *Runtime) Value { return valueProp(valueFloat(math.SmallestNonzeroFloat64), false, false, false) }) + t.putStr("MAX_VALUE", func(r *Runtime) Value { return valueProp(valueFloat(math.MaxFloat64), false, false, false) }) + t.putStr("NaN", func(r *Runtime) Value { return valueProp(_NaN, false, false, false) }) + t.putStr("NEGATIVE_INFINITY", func(r *Runtime) Value { return valueProp(_negativeInf, false, false, false) }) + t.putStr("parseFloat", func(r *Runtime) Value { return valueProp(r.getParseFloat(), true, false, true) }) + t.putStr("parseInt", func(r *Runtime) Value { return valueProp(r.getParseInt(), true, false, true) }) + t.putStr("POSITIVE_INFINITY", func(r *Runtime) Value { return valueProp(_positiveInf, false, false, false) }) + + return t +} + +var numberTemplate *objectTemplate +var numberTemplateOnce sync.Once + +func getNumberTemplate() *objectTemplate { + numberTemplateOnce.Do(func() { + numberTemplate = createNumberTemplate() + }) + return numberTemplate +} + +func (r *Runtime) getNumber() *Object { + ret := r.global.Number + if ret == nil { + ret = &Object{runtime: r} + r.global.Number = ret + r.newTemplatedFuncObject(getNumberTemplate(), ret, r.builtin_Number, + r.wrapNativeConstruct(r.builtin_newNumber, ret, r.getNumberPrototype())) + } + return ret +} diff --git a/goja/builtin_object.go b/goja/builtin_object.go new file mode 100644 index 0000000..6bf1ff8 --- /dev/null +++ b/goja/builtin_object.go @@ -0,0 +1,711 @@ +package goja + +import ( + "fmt" + "sync" +) + +func (r *Runtime) builtin_Object(args []Value, newTarget *Object) *Object { + if newTarget != nil && newTarget != r.getObject() { + proto := r.getPrototypeFromCtor(newTarget, nil, r.global.ObjectPrototype) + return r.newBaseObject(proto, classObject).val + } + if len(args) > 0 { + arg := args[0] + if arg != _undefined && arg != _null { + return arg.ToObject(r) + } + } + return r.NewObject() +} + +func (r *Runtime) object_getPrototypeOf(call FunctionCall) Value { + o := call.Argument(0).ToObject(r) + p := o.self.proto() + if p == nil { + return _null + } + return p +} + +func (r *Runtime) valuePropToDescriptorObject(desc Value) Value { + if desc == nil { + return _undefined + } + var writable, configurable, enumerable, accessor bool + var get, set *Object + var value Value + if v, ok := desc.(*valueProperty); ok { + writable = v.writable + configurable = v.configurable + enumerable = v.enumerable + accessor = v.accessor + value = v.value + get = v.getterFunc + set = v.setterFunc + } else { + writable = true + configurable = true + enumerable = true + value = desc + } + + ret := r.NewObject() + obj := ret.self + if !accessor { + obj.setOwnStr("value", value, false) + obj.setOwnStr("writable", r.toBoolean(writable), false) + } else { + if get != nil { + obj.setOwnStr("get", get, false) + } else { + obj.setOwnStr("get", _undefined, false) + } + if set != nil { + obj.setOwnStr("set", set, false) + } else { + obj.setOwnStr("set", _undefined, false) + } + } + obj.setOwnStr("enumerable", r.toBoolean(enumerable), false) + obj.setOwnStr("configurable", r.toBoolean(configurable), false) + + return ret +} + +func (r *Runtime) object_getOwnPropertyDescriptor(call FunctionCall) Value { + o := call.Argument(0).ToObject(r) + propName := toPropertyKey(call.Argument(1)) + return r.valuePropToDescriptorObject(o.getOwnProp(propName)) +} + +func (r *Runtime) object_getOwnPropertyDescriptors(call FunctionCall) Value { + o := call.Argument(0).ToObject(r) + result := r.newBaseObject(r.global.ObjectPrototype, classObject).val + for item, next := o.self.iterateKeys()(); next != nil; item, next = next() { + var prop Value + if item.value == nil { + prop = o.getOwnProp(item.name) + if prop == nil { + continue + } + } else { + prop = item.value + } + descriptor := r.valuePropToDescriptorObject(prop) + if descriptor != _undefined { + createDataPropertyOrThrow(result, item.name, descriptor) + } + } + return result +} + +func (r *Runtime) object_getOwnPropertyNames(call FunctionCall) Value { + obj := call.Argument(0).ToObject(r) + + return r.newArrayValues(obj.self.stringKeys(true, nil)) +} + +func (r *Runtime) object_getOwnPropertySymbols(call FunctionCall) Value { + obj := call.Argument(0).ToObject(r) + return r.newArrayValues(obj.self.symbols(true, nil)) +} + +func (r *Runtime) toValueProp(v Value) *valueProperty { + if v == nil || v == _undefined { + return nil + } + obj := r.toObject(v) + getter := obj.self.getStr("get", nil) + setter := obj.self.getStr("set", nil) + writable := obj.self.getStr("writable", nil) + value := obj.self.getStr("value", nil) + if (getter != nil || setter != nil) && (value != nil || writable != nil) { + r.typeErrorResult(true, "Invalid property descriptor. Cannot both specify accessors and a value or writable attribute") + } + + ret := &valueProperty{} + if writable != nil && writable.ToBoolean() { + ret.writable = true + } + if e := obj.self.getStr("enumerable", nil); e != nil && e.ToBoolean() { + ret.enumerable = true + } + if c := obj.self.getStr("configurable", nil); c != nil && c.ToBoolean() { + ret.configurable = true + } + ret.value = value + + if getter != nil && getter != _undefined { + o := r.toObject(getter) + if _, ok := o.self.assertCallable(); !ok { + r.typeErrorResult(true, "getter must be a function") + } + ret.getterFunc = o + } + + if setter != nil && setter != _undefined { + o := r.toObject(setter) + if _, ok := o.self.assertCallable(); !ok { + r.typeErrorResult(true, "setter must be a function") + } + ret.setterFunc = o + } + + if ret.getterFunc != nil || ret.setterFunc != nil { + ret.accessor = true + } + + return ret +} + +func (r *Runtime) toPropertyDescriptor(v Value) (ret PropertyDescriptor) { + if o, ok := v.(*Object); ok { + descr := o.self + + // Save the original descriptor for reference + ret.jsDescriptor = o + + ret.Value = descr.getStr("value", nil) + + if p := descr.getStr("writable", nil); p != nil { + ret.Writable = ToFlag(p.ToBoolean()) + } + if p := descr.getStr("enumerable", nil); p != nil { + ret.Enumerable = ToFlag(p.ToBoolean()) + } + if p := descr.getStr("configurable", nil); p != nil { + ret.Configurable = ToFlag(p.ToBoolean()) + } + + ret.Getter = descr.getStr("get", nil) + ret.Setter = descr.getStr("set", nil) + + if ret.Getter != nil && ret.Getter != _undefined { + if _, ok := r.toObject(ret.Getter).self.assertCallable(); !ok { + r.typeErrorResult(true, "getter must be a function") + } + } + + if ret.Setter != nil && ret.Setter != _undefined { + if _, ok := r.toObject(ret.Setter).self.assertCallable(); !ok { + r.typeErrorResult(true, "setter must be a function") + } + } + + if (ret.Getter != nil || ret.Setter != nil) && (ret.Value != nil || ret.Writable != FLAG_NOT_SET) { + r.typeErrorResult(true, "Invalid property descriptor. Cannot both specify accessors and a value or writable attribute") + } + } else { + r.typeErrorResult(true, "Property description must be an object: %s", v.String()) + } + + return +} + +func (r *Runtime) _defineProperties(o *Object, p Value) { + type propItem struct { + name Value + prop PropertyDescriptor + } + props := p.ToObject(r) + var list []propItem + for item, next := iterateEnumerableProperties(props)(); next != nil; item, next = next() { + list = append(list, propItem{ + name: item.name, + prop: r.toPropertyDescriptor(item.value), + }) + } + for _, prop := range list { + o.defineOwnProperty(prop.name, prop.prop, true) + } +} + +func (r *Runtime) object_create(call FunctionCall) Value { + var proto *Object + if arg := call.Argument(0); arg != _null { + if o, ok := arg.(*Object); ok { + proto = o + } else { + r.typeErrorResult(true, "Object prototype may only be an Object or null: %s", arg.String()) + } + } + o := r.newBaseObject(proto, classObject).val + + if props := call.Argument(1); props != _undefined { + r._defineProperties(o, props) + } + + return o +} + +func (r *Runtime) object_defineProperty(call FunctionCall) (ret Value) { + if obj, ok := call.Argument(0).(*Object); ok { + descr := r.toPropertyDescriptor(call.Argument(2)) + obj.defineOwnProperty(toPropertyKey(call.Argument(1)), descr, true) + ret = call.Argument(0) + } else { + r.typeErrorResult(true, "Object.defineProperty called on non-object") + } + return +} + +func (r *Runtime) object_defineProperties(call FunctionCall) Value { + obj := r.toObject(call.Argument(0)) + r._defineProperties(obj, call.Argument(1)) + return obj +} + +func (r *Runtime) object_seal(call FunctionCall) Value { + // ES6 + arg := call.Argument(0) + if obj, ok := arg.(*Object); ok { + obj.self.preventExtensions(true) + descr := PropertyDescriptor{ + Configurable: FLAG_FALSE, + } + + for item, next := obj.self.iterateKeys()(); next != nil; item, next = next() { + if prop, ok := item.value.(*valueProperty); ok { + prop.configurable = false + } else { + obj.defineOwnProperty(item.name, descr, true) + } + } + + return obj + } + return arg +} + +func (r *Runtime) object_freeze(call FunctionCall) Value { + arg := call.Argument(0) + if obj, ok := arg.(*Object); ok { + obj.self.preventExtensions(true) + + for item, next := obj.self.iterateKeys()(); next != nil; item, next = next() { + if prop, ok := item.value.(*valueProperty); ok { + prop.configurable = false + if !prop.accessor { + prop.writable = false + } + } else { + prop := obj.getOwnProp(item.name) + descr := PropertyDescriptor{ + Configurable: FLAG_FALSE, + } + if prop, ok := prop.(*valueProperty); ok && prop.accessor { + // no-op + } else { + descr.Writable = FLAG_FALSE + } + obj.defineOwnProperty(item.name, descr, true) + } + } + return obj + } else { + // ES6 behavior + return arg + } +} + +func (r *Runtime) object_preventExtensions(call FunctionCall) (ret Value) { + arg := call.Argument(0) + if obj, ok := arg.(*Object); ok { + obj.self.preventExtensions(true) + } + return arg +} + +func (r *Runtime) object_isSealed(call FunctionCall) Value { + if obj, ok := call.Argument(0).(*Object); ok { + if obj.self.isExtensible() { + return valueFalse + } + for item, next := obj.self.iterateKeys()(); next != nil; item, next = next() { + var prop Value + if item.value == nil { + prop = obj.getOwnProp(item.name) + if prop == nil { + continue + } + } else { + prop = item.value + } + if prop, ok := prop.(*valueProperty); ok { + if prop.configurable { + return valueFalse + } + } else { + return valueFalse + } + } + } + return valueTrue +} + +func (r *Runtime) object_isFrozen(call FunctionCall) Value { + if obj, ok := call.Argument(0).(*Object); ok { + if obj.self.isExtensible() { + return valueFalse + } + for item, next := obj.self.iterateKeys()(); next != nil; item, next = next() { + var prop Value + if item.value == nil { + prop = obj.getOwnProp(item.name) + if prop == nil { + continue + } + } else { + prop = item.value + } + if prop, ok := prop.(*valueProperty); ok { + if prop.configurable || prop.value != nil && prop.writable { + return valueFalse + } + } else { + return valueFalse + } + } + } + return valueTrue +} + +func (r *Runtime) object_isExtensible(call FunctionCall) Value { + if obj, ok := call.Argument(0).(*Object); ok { + if obj.self.isExtensible() { + return valueTrue + } + return valueFalse + } else { + // ES6 + //r.typeErrorResult(true, "Object.isExtensible called on non-object") + return valueFalse + } +} + +func (r *Runtime) object_keys(call FunctionCall) Value { + obj := call.Argument(0).ToObject(r) + + return r.newArrayValues(obj.self.stringKeys(false, nil)) +} + +func (r *Runtime) object_entries(call FunctionCall) Value { + obj := call.Argument(0).ToObject(r) + + var values []Value + + for item, next := iterateEnumerableStringProperties(obj)(); next != nil; item, next = next() { + values = append(values, r.newArrayValues([]Value{item.name, item.value})) + } + + return r.newArrayValues(values) +} + +func (r *Runtime) object_values(call FunctionCall) Value { + obj := call.Argument(0).ToObject(r) + + var values []Value + + for item, next := iterateEnumerableStringProperties(obj)(); next != nil; item, next = next() { + values = append(values, item.value) + } + + return r.newArrayValues(values) +} + +func (r *Runtime) objectproto_hasOwnProperty(call FunctionCall) Value { + p := toPropertyKey(call.Argument(0)) + o := call.This.ToObject(r) + if o.hasOwnProperty(p) { + return valueTrue + } else { + return valueFalse + } +} + +func (r *Runtime) objectproto_isPrototypeOf(call FunctionCall) Value { + if v, ok := call.Argument(0).(*Object); ok { + o := call.This.ToObject(r) + for { + v = v.self.proto() + if v == nil { + break + } + if v == o { + return valueTrue + } + } + } + return valueFalse +} + +func (r *Runtime) objectproto_propertyIsEnumerable(call FunctionCall) Value { + p := toPropertyKey(call.Argument(0)) + o := call.This.ToObject(r) + pv := o.getOwnProp(p) + if pv == nil { + return valueFalse + } + if prop, ok := pv.(*valueProperty); ok { + if !prop.enumerable { + return valueFalse + } + } + return valueTrue +} + +func (r *Runtime) objectproto_toString(call FunctionCall) Value { + switch o := call.This.(type) { + case valueNull: + return stringObjectNull + case valueUndefined: + return stringObjectUndefined + default: + obj := o.ToObject(r) + if o, ok := obj.self.(*objectGoReflect); ok { + if toString := o.toString; toString != nil { + return toString() + } + } + var clsName string + if isArray(obj) { + clsName = classArray + } else { + clsName = obj.self.className() + } + if tag := obj.self.getSym(SymToStringTag, nil); tag != nil { + if str, ok := tag.(String); ok { + clsName = str.String() + } + } + return newStringValue(fmt.Sprintf("[object %s]", clsName)) + } +} + +func (r *Runtime) objectproto_toLocaleString(call FunctionCall) Value { + toString := toMethod(r.getVStr(call.This, "toString")) + return toString(FunctionCall{This: call.This}) +} + +func (r *Runtime) objectproto_getProto(call FunctionCall) Value { + proto := call.This.ToObject(r).self.proto() + if proto != nil { + return proto + } + return _null +} + +func (r *Runtime) setObjectProto(o, arg Value) { + r.checkObjectCoercible(o) + var proto *Object + if arg != _null { + if obj, ok := arg.(*Object); ok { + proto = obj + } else { + return + } + } + if o, ok := o.(*Object); ok { + o.self.setProto(proto, true) + } +} + +func (r *Runtime) objectproto_setProto(call FunctionCall) Value { + r.setObjectProto(call.This, call.Argument(0)) + return _undefined +} + +func (r *Runtime) objectproto_valueOf(call FunctionCall) Value { + return call.This.ToObject(r) +} + +func (r *Runtime) object_assign(call FunctionCall) Value { + to := call.Argument(0).ToObject(r) + if len(call.Arguments) > 1 { + for _, arg := range call.Arguments[1:] { + if arg != _undefined && arg != _null { + source := arg.ToObject(r) + for item, next := iterateEnumerableProperties(source)(); next != nil; item, next = next() { + to.setOwn(item.name, item.value, true) + } + } + } + } + + return to +} + +func (r *Runtime) object_is(call FunctionCall) Value { + return r.toBoolean(call.Argument(0).SameAs(call.Argument(1))) +} + +func (r *Runtime) toProto(proto Value) *Object { + if proto != _null { + if obj, ok := proto.(*Object); ok { + return obj + } else { + panic(r.NewTypeError("Object prototype may only be an Object or null: %s", proto)) + } + } + return nil +} + +func (r *Runtime) object_setPrototypeOf(call FunctionCall) Value { + o := call.Argument(0) + r.checkObjectCoercible(o) + proto := r.toProto(call.Argument(1)) + if o, ok := o.(*Object); ok { + o.self.setProto(proto, true) + } + + return o +} + +func (r *Runtime) object_fromEntries(call FunctionCall) Value { + o := call.Argument(0) + r.checkObjectCoercible(o) + + result := r.newBaseObject(r.global.ObjectPrototype, classObject).val + + iter := r.getIterator(o, nil) + iter.iterate(func(nextValue Value) { + i0 := valueInt(0) + i1 := valueInt(1) + + itemObj := r.toObject(nextValue) + k := itemObj.self.getIdx(i0, nil) + v := itemObj.self.getIdx(i1, nil) + key := toPropertyKey(k) + + createDataPropertyOrThrow(result, key, v) + }) + + return result +} + +func (r *Runtime) object_hasOwn(call FunctionCall) Value { + o := call.Argument(0) + obj := o.ToObject(r) + p := toPropertyKey(call.Argument(1)) + + if obj.hasOwnProperty(p) { + return valueTrue + } else { + return valueFalse + } +} + +func createObjectTemplate() *objectTemplate { + t := newObjectTemplate() + t.protoFactory = func(r *Runtime) *Object { + return r.getFunctionPrototype() + } + + t.putStr("length", func(r *Runtime) Value { return valueProp(intToValue(1), false, false, true) }) + t.putStr("name", func(r *Runtime) Value { return valueProp(asciiString("Object"), false, false, true) }) + + t.putStr("prototype", func(r *Runtime) Value { return valueProp(r.global.ObjectPrototype, false, false, false) }) + + t.putStr("assign", func(r *Runtime) Value { return r.methodProp(r.object_assign, "assign", 2) }) + t.putStr("defineProperty", func(r *Runtime) Value { return r.methodProp(r.object_defineProperty, "defineProperty", 3) }) + t.putStr("defineProperties", func(r *Runtime) Value { return r.methodProp(r.object_defineProperties, "defineProperties", 2) }) + t.putStr("entries", func(r *Runtime) Value { return r.methodProp(r.object_entries, "entries", 1) }) + t.putStr("getOwnPropertyDescriptor", func(r *Runtime) Value { + return r.methodProp(r.object_getOwnPropertyDescriptor, "getOwnPropertyDescriptor", 2) + }) + t.putStr("getOwnPropertyDescriptors", func(r *Runtime) Value { + return r.methodProp(r.object_getOwnPropertyDescriptors, "getOwnPropertyDescriptors", 1) + }) + t.putStr("getPrototypeOf", func(r *Runtime) Value { return r.methodProp(r.object_getPrototypeOf, "getPrototypeOf", 1) }) + t.putStr("is", func(r *Runtime) Value { return r.methodProp(r.object_is, "is", 2) }) + t.putStr("getOwnPropertyNames", func(r *Runtime) Value { return r.methodProp(r.object_getOwnPropertyNames, "getOwnPropertyNames", 1) }) + t.putStr("getOwnPropertySymbols", func(r *Runtime) Value { + return r.methodProp(r.object_getOwnPropertySymbols, "getOwnPropertySymbols", 1) + }) + t.putStr("create", func(r *Runtime) Value { return r.methodProp(r.object_create, "create", 2) }) + t.putStr("seal", func(r *Runtime) Value { return r.methodProp(r.object_seal, "seal", 1) }) + t.putStr("freeze", func(r *Runtime) Value { return r.methodProp(r.object_freeze, "freeze", 1) }) + t.putStr("preventExtensions", func(r *Runtime) Value { return r.methodProp(r.object_preventExtensions, "preventExtensions", 1) }) + t.putStr("isSealed", func(r *Runtime) Value { return r.methodProp(r.object_isSealed, "isSealed", 1) }) + t.putStr("isFrozen", func(r *Runtime) Value { return r.methodProp(r.object_isFrozen, "isFrozen", 1) }) + t.putStr("isExtensible", func(r *Runtime) Value { return r.methodProp(r.object_isExtensible, "isExtensible", 1) }) + t.putStr("keys", func(r *Runtime) Value { return r.methodProp(r.object_keys, "keys", 1) }) + t.putStr("setPrototypeOf", func(r *Runtime) Value { return r.methodProp(r.object_setPrototypeOf, "setPrototypeOf", 2) }) + t.putStr("values", func(r *Runtime) Value { return r.methodProp(r.object_values, "values", 1) }) + t.putStr("fromEntries", func(r *Runtime) Value { return r.methodProp(r.object_fromEntries, "fromEntries", 1) }) + t.putStr("hasOwn", func(r *Runtime) Value { return r.methodProp(r.object_hasOwn, "hasOwn", 2) }) + + return t +} + +var _objectTemplate *objectTemplate +var objectTemplateOnce sync.Once + +func getObjectTemplate() *objectTemplate { + objectTemplateOnce.Do(func() { + _objectTemplate = createObjectTemplate() + }) + return _objectTemplate +} + +func (r *Runtime) getObject() *Object { + ret := r.global.Object + if ret == nil { + ret = &Object{runtime: r} + r.global.Object = ret + r.newTemplatedFuncObject(getObjectTemplate(), ret, func(call FunctionCall) Value { + return r.builtin_Object(call.Arguments, nil) + }, r.builtin_Object) + } + return ret +} + +/* +func (r *Runtime) getObjectPrototype() *Object { + ret := r.global.ObjectPrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.ObjectPrototype = ret + r.newTemplatedObject(getObjectProtoTemplate(), ret) + } + return ret +} +*/ + +var objectProtoTemplate *objectTemplate +var objectProtoTemplateOnce sync.Once + +func getObjectProtoTemplate() *objectTemplate { + objectProtoTemplateOnce.Do(func() { + objectProtoTemplate = createObjectProtoTemplate() + }) + return objectProtoTemplate +} + +func createObjectProtoTemplate() *objectTemplate { + t := newObjectTemplate() + + // null prototype + + t.putStr("constructor", func(r *Runtime) Value { return valueProp(r.getObject(), true, false, true) }) + + t.putStr("toString", func(r *Runtime) Value { return r.methodProp(r.objectproto_toString, "toString", 0) }) + t.putStr("toLocaleString", func(r *Runtime) Value { return r.methodProp(r.objectproto_toLocaleString, "toLocaleString", 0) }) + t.putStr("valueOf", func(r *Runtime) Value { return r.methodProp(r.objectproto_valueOf, "valueOf", 0) }) + t.putStr("hasOwnProperty", func(r *Runtime) Value { return r.methodProp(r.objectproto_hasOwnProperty, "hasOwnProperty", 1) }) + t.putStr("isPrototypeOf", func(r *Runtime) Value { return r.methodProp(r.objectproto_isPrototypeOf, "isPrototypeOf", 1) }) + t.putStr("propertyIsEnumerable", func(r *Runtime) Value { + return r.methodProp(r.objectproto_propertyIsEnumerable, "propertyIsEnumerable", 1) + }) + t.putStr(__proto__, func(r *Runtime) Value { + return &valueProperty{ + accessor: true, + getterFunc: r.newNativeFunc(r.objectproto_getProto, "get __proto__", 0), + setterFunc: r.newNativeFunc(r.objectproto_setProto, "set __proto__", 1), + configurable: true, + } + }) + + return t +} diff --git a/goja/builtin_promise.go b/goja/builtin_promise.go new file mode 100644 index 0000000..d51f27d --- /dev/null +++ b/goja/builtin_promise.go @@ -0,0 +1,646 @@ +package goja + +import ( + "github.com/dop251/goja/unistring" + "reflect" +) + +type PromiseState int +type PromiseRejectionOperation int + +type promiseReactionType int + +const ( + PromiseStatePending PromiseState = iota + PromiseStateFulfilled + PromiseStateRejected +) + +const ( + PromiseRejectionReject PromiseRejectionOperation = iota + PromiseRejectionHandle +) + +const ( + promiseReactionFulfill promiseReactionType = iota + promiseReactionReject +) + +type PromiseRejectionTracker func(p *Promise, operation PromiseRejectionOperation) + +type jobCallback struct { + callback func(FunctionCall) Value +} + +type promiseCapability struct { + promise *Object + resolveObj, rejectObj *Object +} + +type promiseReaction struct { + capability *promiseCapability + typ promiseReactionType + handler *jobCallback + asyncRunner *asyncRunner + asyncCtx interface{} +} + +var typePromise = reflect.TypeOf((*Promise)(nil)) + +// Promise is a Go wrapper around ECMAScript Promise. Calling Runtime.ToValue() on it +// returns the underlying Object. Calling Export() on a Promise Object returns a Promise. +// +// Use Runtime.NewPromise() to create one. Calling Runtime.ToValue() on a zero object or nil returns null Value. +// +// WARNING: Instances of Promise are not goroutine-safe. See Runtime.NewPromise() for more details. +type Promise struct { + baseObject + state PromiseState + result Value + fulfillReactions []*promiseReaction + rejectReactions []*promiseReaction + handled bool +} + +func (p *Promise) State() PromiseState { + return p.state +} + +func (p *Promise) Result() Value { + return p.result +} + +func (p *Promise) toValue(r *Runtime) Value { + if p == nil || p.val == nil { + return _null + } + promise := p.val + if promise.runtime != r { + panic(r.NewTypeError("Illegal runtime transition of a Promise")) + } + return promise +} + +func (p *Promise) createResolvingFunctions() (resolve, reject *Object) { + r := p.val.runtime + alreadyResolved := false + return p.val.runtime.newNativeFunc(func(call FunctionCall) Value { + if alreadyResolved { + return _undefined + } + alreadyResolved = true + resolution := call.Argument(0) + if resolution.SameAs(p.val) { + return p.reject(r.NewTypeError("Promise self-resolution")) + } + if obj, ok := resolution.(*Object); ok { + var thenAction Value + ex := r.vm.try(func() { + thenAction = obj.self.getStr("then", nil) + }) + if ex != nil { + return p.reject(ex.val) + } + if call, ok := assertCallable(thenAction); ok { + job := r.newPromiseResolveThenableJob(p, resolution, &jobCallback{callback: call}) + r.enqueuePromiseJob(job) + return _undefined + } + } + return p.fulfill(resolution) + }, "", 1), + p.val.runtime.newNativeFunc(func(call FunctionCall) Value { + if alreadyResolved { + return _undefined + } + alreadyResolved = true + reason := call.Argument(0) + return p.reject(reason) + }, "", 1) +} + +func (p *Promise) reject(reason Value) Value { + reactions := p.rejectReactions + p.result = reason + p.fulfillReactions, p.rejectReactions = nil, nil + p.state = PromiseStateRejected + r := p.val.runtime + if !p.handled { + r.trackPromiseRejection(p, PromiseRejectionReject) + } + r.triggerPromiseReactions(reactions, reason) + return _undefined +} + +func (p *Promise) fulfill(value Value) Value { + reactions := p.fulfillReactions + p.result = value + p.fulfillReactions, p.rejectReactions = nil, nil + p.state = PromiseStateFulfilled + p.val.runtime.triggerPromiseReactions(reactions, value) + return _undefined +} + +func (p *Promise) exportType() reflect.Type { + return typePromise +} + +func (p *Promise) export(*objectExportCtx) interface{} { + return p +} + +func (p *Promise) addReactions(fulfillReaction *promiseReaction, rejectReaction *promiseReaction) { + r := p.val.runtime + if tracker := r.asyncContextTracker; tracker != nil { + ctx := tracker.Grab() + fulfillReaction.asyncCtx = ctx + rejectReaction.asyncCtx = ctx + } + switch p.state { + case PromiseStatePending: + p.fulfillReactions = append(p.fulfillReactions, fulfillReaction) + p.rejectReactions = append(p.rejectReactions, rejectReaction) + case PromiseStateFulfilled: + r.enqueuePromiseJob(r.newPromiseReactionJob(fulfillReaction, p.result)) + default: + reason := p.result + if !p.handled { + r.trackPromiseRejection(p, PromiseRejectionHandle) + } + r.enqueuePromiseJob(r.newPromiseReactionJob(rejectReaction, reason)) + } + p.handled = true +} + +func (r *Runtime) newPromiseResolveThenableJob(p *Promise, thenable Value, then *jobCallback) func() { + return func() { + resolve, reject := p.createResolvingFunctions() + ex := r.vm.try(func() { + r.callJobCallback(then, thenable, resolve, reject) + }) + if ex != nil { + if fn, ok := reject.self.assertCallable(); ok { + fn(FunctionCall{Arguments: []Value{ex.val}}) + } + } + } +} + +func (r *Runtime) enqueuePromiseJob(job func()) { + r.jobQueue = append(r.jobQueue, job) +} + +func (r *Runtime) triggerPromiseReactions(reactions []*promiseReaction, argument Value) { + for _, reaction := range reactions { + r.enqueuePromiseJob(r.newPromiseReactionJob(reaction, argument)) + } +} + +func (r *Runtime) newPromiseReactionJob(reaction *promiseReaction, argument Value) func() { + return func() { + var handlerResult Value + fulfill := false + if reaction.handler == nil { + handlerResult = argument + if reaction.typ == promiseReactionFulfill { + fulfill = true + } + } else { + if tracker := r.asyncContextTracker; tracker != nil { + tracker.Resumed(reaction.asyncCtx) + } + ex := r.vm.try(func() { + handlerResult = r.callJobCallback(reaction.handler, _undefined, argument) + fulfill = true + }) + if ex != nil { + handlerResult = ex.val + } + if tracker := r.asyncContextTracker; tracker != nil { + tracker.Exited() + } + } + if reaction.capability != nil { + if fulfill { + reaction.capability.resolve(handlerResult) + } else { + reaction.capability.reject(handlerResult) + } + } + } +} + +func (r *Runtime) newPromise(proto *Object) *Promise { + o := &Object{runtime: r} + + po := &Promise{} + po.class = classObject + po.val = o + po.extensible = true + o.self = po + po.prototype = proto + po.init() + return po +} + +func (r *Runtime) builtin_newPromise(args []Value, newTarget *Object) *Object { + if newTarget == nil { + panic(r.needNew("Promise")) + } + var arg0 Value + if len(args) > 0 { + arg0 = args[0] + } + executor := r.toCallable(arg0) + + proto := r.getPrototypeFromCtor(newTarget, r.global.Promise, r.getPromisePrototype()) + po := r.newPromise(proto) + + resolve, reject := po.createResolvingFunctions() + ex := r.vm.try(func() { + executor(FunctionCall{Arguments: []Value{resolve, reject}}) + }) + if ex != nil { + if fn, ok := reject.self.assertCallable(); ok { + fn(FunctionCall{Arguments: []Value{ex.val}}) + } + } + return po.val +} + +func (r *Runtime) promiseProto_then(call FunctionCall) Value { + thisObj := r.toObject(call.This) + if p, ok := thisObj.self.(*Promise); ok { + c := r.speciesConstructorObj(thisObj, r.getPromise()) + resultCapability := r.newPromiseCapability(c) + return r.performPromiseThen(p, call.Argument(0), call.Argument(1), resultCapability) + } + panic(r.NewTypeError("Method Promise.prototype.then called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) +} + +func (r *Runtime) newPromiseCapability(c *Object) *promiseCapability { + pcap := new(promiseCapability) + if c == r.getPromise() { + p := r.newPromise(r.getPromisePrototype()) + pcap.resolveObj, pcap.rejectObj = p.createResolvingFunctions() + pcap.promise = p.val + } else { + var resolve, reject Value + executor := r.newNativeFunc(func(call FunctionCall) Value { + if resolve != nil { + panic(r.NewTypeError("resolve is already set")) + } + if reject != nil { + panic(r.NewTypeError("reject is already set")) + } + if arg := call.Argument(0); arg != _undefined { + resolve = arg + } + if arg := call.Argument(1); arg != _undefined { + reject = arg + } + return nil + }, "", 2) + pcap.promise = r.toConstructor(c)([]Value{executor}, c) + pcap.resolveObj = r.toObject(resolve) + r.toCallable(pcap.resolveObj) // make sure it's callable + pcap.rejectObj = r.toObject(reject) + r.toCallable(pcap.rejectObj) + } + return pcap +} + +func (r *Runtime) performPromiseThen(p *Promise, onFulfilled, onRejected Value, resultCapability *promiseCapability) Value { + var onFulfilledJobCallback, onRejectedJobCallback *jobCallback + if f, ok := assertCallable(onFulfilled); ok { + onFulfilledJobCallback = &jobCallback{callback: f} + } + if f, ok := assertCallable(onRejected); ok { + onRejectedJobCallback = &jobCallback{callback: f} + } + fulfillReaction := &promiseReaction{ + capability: resultCapability, + typ: promiseReactionFulfill, + handler: onFulfilledJobCallback, + } + rejectReaction := &promiseReaction{ + capability: resultCapability, + typ: promiseReactionReject, + handler: onRejectedJobCallback, + } + p.addReactions(fulfillReaction, rejectReaction) + if resultCapability == nil { + return _undefined + } + return resultCapability.promise +} + +func (r *Runtime) promiseProto_catch(call FunctionCall) Value { + return r.invoke(call.This, "then", _undefined, call.Argument(0)) +} + +func (r *Runtime) promiseResolve(c *Object, x Value) *Object { + if obj, ok := x.(*Object); ok { + xConstructor := nilSafe(obj.self.getStr("constructor", nil)) + if xConstructor.SameAs(c) { + return obj + } + } + pcap := r.newPromiseCapability(c) + pcap.resolve(x) + return pcap.promise +} + +func (r *Runtime) promiseProto_finally(call FunctionCall) Value { + promise := r.toObject(call.This) + c := r.speciesConstructorObj(promise, r.getPromise()) + onFinally := call.Argument(0) + var thenFinally, catchFinally Value + if onFinallyFn, ok := assertCallable(onFinally); !ok { + thenFinally, catchFinally = onFinally, onFinally + } else { + thenFinally = r.newNativeFunc(func(call FunctionCall) Value { + value := call.Argument(0) + result := onFinallyFn(FunctionCall{}) + promise := r.promiseResolve(c, result) + valueThunk := r.newNativeFunc(func(call FunctionCall) Value { + return value + }, "", 0) + return r.invoke(promise, "then", valueThunk) + }, "", 1) + + catchFinally = r.newNativeFunc(func(call FunctionCall) Value { + reason := call.Argument(0) + result := onFinallyFn(FunctionCall{}) + promise := r.promiseResolve(c, result) + thrower := r.newNativeFunc(func(call FunctionCall) Value { + panic(reason) + }, "", 0) + return r.invoke(promise, "then", thrower) + }, "", 1) + } + return r.invoke(promise, "then", thenFinally, catchFinally) +} + +func (pcap *promiseCapability) resolve(result Value) { + pcap.promise.runtime.toCallable(pcap.resolveObj)(FunctionCall{Arguments: []Value{result}}) +} + +func (pcap *promiseCapability) reject(reason Value) { + pcap.promise.runtime.toCallable(pcap.rejectObj)(FunctionCall{Arguments: []Value{reason}}) +} + +func (pcap *promiseCapability) try(f func()) bool { + ex := pcap.promise.runtime.vm.try(f) + if ex != nil { + pcap.reject(ex.val) + return false + } + return true +} + +func (r *Runtime) promise_all(call FunctionCall) Value { + c := r.toObject(call.This) + pcap := r.newPromiseCapability(c) + + pcap.try(func() { + promiseResolve := r.toCallable(c.self.getStr("resolve", nil)) + iter := r.getIterator(call.Argument(0), nil) + var values []Value + remainingElementsCount := 1 + iter.iterate(func(nextValue Value) { + index := len(values) + values = append(values, _undefined) + nextPromise := promiseResolve(FunctionCall{This: c, Arguments: []Value{nextValue}}) + alreadyCalled := false + onFulfilled := r.newNativeFunc(func(call FunctionCall) Value { + if alreadyCalled { + return _undefined + } + alreadyCalled = true + values[index] = call.Argument(0) + remainingElementsCount-- + if remainingElementsCount == 0 { + pcap.resolve(r.newArrayValues(values)) + } + return _undefined + }, "", 1) + remainingElementsCount++ + r.invoke(nextPromise, "then", onFulfilled, pcap.rejectObj) + }) + remainingElementsCount-- + if remainingElementsCount == 0 { + pcap.resolve(r.newArrayValues(values)) + } + }) + return pcap.promise +} + +func (r *Runtime) promise_allSettled(call FunctionCall) Value { + c := r.toObject(call.This) + pcap := r.newPromiseCapability(c) + + pcap.try(func() { + promiseResolve := r.toCallable(c.self.getStr("resolve", nil)) + iter := r.getIterator(call.Argument(0), nil) + var values []Value + remainingElementsCount := 1 + iter.iterate(func(nextValue Value) { + index := len(values) + values = append(values, _undefined) + nextPromise := promiseResolve(FunctionCall{This: c, Arguments: []Value{nextValue}}) + alreadyCalled := false + reaction := func(status Value, valueKey unistring.String) *Object { + return r.newNativeFunc(func(call FunctionCall) Value { + if alreadyCalled { + return _undefined + } + alreadyCalled = true + obj := r.NewObject() + obj.self._putProp("status", status, true, true, true) + obj.self._putProp(valueKey, call.Argument(0), true, true, true) + values[index] = obj + remainingElementsCount-- + if remainingElementsCount == 0 { + pcap.resolve(r.newArrayValues(values)) + } + return _undefined + }, "", 1) + } + onFulfilled := reaction(asciiString("fulfilled"), "value") + onRejected := reaction(asciiString("rejected"), "reason") + remainingElementsCount++ + r.invoke(nextPromise, "then", onFulfilled, onRejected) + }) + remainingElementsCount-- + if remainingElementsCount == 0 { + pcap.resolve(r.newArrayValues(values)) + } + }) + return pcap.promise +} + +func (r *Runtime) promise_any(call FunctionCall) Value { + c := r.toObject(call.This) + pcap := r.newPromiseCapability(c) + + pcap.try(func() { + promiseResolve := r.toCallable(c.self.getStr("resolve", nil)) + iter := r.getIterator(call.Argument(0), nil) + var errors []Value + remainingElementsCount := 1 + iter.iterate(func(nextValue Value) { + index := len(errors) + errors = append(errors, _undefined) + nextPromise := promiseResolve(FunctionCall{This: c, Arguments: []Value{nextValue}}) + alreadyCalled := false + onRejected := r.newNativeFunc(func(call FunctionCall) Value { + if alreadyCalled { + return _undefined + } + alreadyCalled = true + errors[index] = call.Argument(0) + remainingElementsCount-- + if remainingElementsCount == 0 { + _error := r.builtin_new(r.getAggregateError(), nil) + _error.self._putProp("errors", r.newArrayValues(errors), true, false, true) + pcap.reject(_error) + } + return _undefined + }, "", 1) + + remainingElementsCount++ + r.invoke(nextPromise, "then", pcap.resolveObj, onRejected) + }) + remainingElementsCount-- + if remainingElementsCount == 0 { + _error := r.builtin_new(r.getAggregateError(), nil) + _error.self._putProp("errors", r.newArrayValues(errors), true, false, true) + pcap.reject(_error) + } + }) + return pcap.promise +} + +func (r *Runtime) promise_race(call FunctionCall) Value { + c := r.toObject(call.This) + pcap := r.newPromiseCapability(c) + + pcap.try(func() { + promiseResolve := r.toCallable(c.self.getStr("resolve", nil)) + iter := r.getIterator(call.Argument(0), nil) + iter.iterate(func(nextValue Value) { + nextPromise := promiseResolve(FunctionCall{This: c, Arguments: []Value{nextValue}}) + r.invoke(nextPromise, "then", pcap.resolveObj, pcap.rejectObj) + }) + }) + return pcap.promise +} + +func (r *Runtime) promise_reject(call FunctionCall) Value { + pcap := r.newPromiseCapability(r.toObject(call.This)) + pcap.reject(call.Argument(0)) + return pcap.promise +} + +func (r *Runtime) promise_resolve(call FunctionCall) Value { + return r.promiseResolve(r.toObject(call.This), call.Argument(0)) +} + +func (r *Runtime) createPromiseProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.global.ObjectPrototype, classObject) + o._putProp("constructor", r.getPromise(), true, false, true) + + o._putProp("catch", r.newNativeFunc(r.promiseProto_catch, "catch", 1), true, false, true) + o._putProp("finally", r.newNativeFunc(r.promiseProto_finally, "finally", 1), true, false, true) + o._putProp("then", r.newNativeFunc(r.promiseProto_then, "then", 2), true, false, true) + + o._putSym(SymToStringTag, valueProp(asciiString(classPromise), false, false, true)) + + return o +} + +func (r *Runtime) createPromise(val *Object) objectImpl { + o := r.newNativeConstructOnly(val, r.builtin_newPromise, r.getPromisePrototype(), "Promise", 1) + + o._putProp("all", r.newNativeFunc(r.promise_all, "all", 1), true, false, true) + o._putProp("allSettled", r.newNativeFunc(r.promise_allSettled, "allSettled", 1), true, false, true) + o._putProp("any", r.newNativeFunc(r.promise_any, "any", 1), true, false, true) + o._putProp("race", r.newNativeFunc(r.promise_race, "race", 1), true, false, true) + o._putProp("reject", r.newNativeFunc(r.promise_reject, "reject", 1), true, false, true) + o._putProp("resolve", r.newNativeFunc(r.promise_resolve, "resolve", 1), true, false, true) + + r.putSpeciesReturnThis(o) + + return o +} + +func (r *Runtime) getPromisePrototype() *Object { + ret := r.global.PromisePrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.PromisePrototype = ret + ret.self = r.createPromiseProto(ret) + } + return ret +} + +func (r *Runtime) getPromise() *Object { + ret := r.global.Promise + if ret == nil { + ret = &Object{runtime: r} + r.global.Promise = ret + ret.self = r.createPromise(ret) + } + return ret +} + +func (r *Runtime) wrapPromiseReaction(fObj *Object) func(interface{}) { + f, _ := AssertFunction(fObj) + return func(x interface{}) { + _, _ = f(nil, r.ToValue(x)) + } +} + +// NewPromise creates and returns a Promise and resolving functions for it. +// +// WARNING: The returned values are not goroutine-safe and must not be called in parallel with VM running. +// In order to make use of this method you need an event loop such as the one in goja_nodejs (https://github.com/dop251/goja_nodejs) +// where it can be used like this: +// +// loop := NewEventLoop() +// loop.Start() +// defer loop.Stop() +// loop.RunOnLoop(func(vm *goja.Runtime) { +// p, resolve, _ := vm.NewPromise() +// vm.Set("p", p) +// go func() { +// time.Sleep(500 * time.Millisecond) // or perform any other blocking operation +// loop.RunOnLoop(func(*goja.Runtime) { // resolve() must be called on the loop, cannot call it here +// resolve(result) +// }) +// }() +// } +func (r *Runtime) NewPromise() (promise *Promise, resolve func(result interface{}), reject func(reason interface{})) { + p := r.newPromise(r.getPromisePrototype()) + resolveF, rejectF := p.createResolvingFunctions() + return p, r.wrapPromiseReaction(resolveF), r.wrapPromiseReaction(rejectF) +} + +// SetPromiseRejectionTracker registers a function that will be called in two scenarios: when a promise is rejected +// without any handlers (with operation argument set to PromiseRejectionReject), and when a handler is added to a +// rejected promise for the first time (with operation argument set to PromiseRejectionHandle). +// +// Setting a tracker replaces any existing one. Setting it to nil disables the functionality. +// +// See https://tc39.es/ecma262/#sec-host-promise-rejection-tracker for more details. +func (r *Runtime) SetPromiseRejectionTracker(tracker PromiseRejectionTracker) { + r.promiseRejectionTracker = tracker +} + +// SetAsyncContextTracker registers a handler that allows to track async execution contexts. See AsyncContextTracker +// documentation for more details. Setting it to nil disables the functionality. +// This method (as Runtime in general) is not goroutine-safe. +func (r *Runtime) SetAsyncContextTracker(tracker AsyncContextTracker) { + r.asyncContextTracker = tracker +} diff --git a/goja/builtin_proxy.go b/goja/builtin_proxy.go new file mode 100644 index 0000000..f589930 --- /dev/null +++ b/goja/builtin_proxy.go @@ -0,0 +1,396 @@ +package goja + +import ( + "github.com/dop251/goja/unistring" +) + +type nativeProxyHandler struct { + handler *ProxyTrapConfig +} + +func (h *nativeProxyHandler) getPrototypeOf(target *Object) (Value, bool) { + if trap := h.handler.GetPrototypeOf; trap != nil { + return trap(target), true + } + return nil, false +} + +func (h *nativeProxyHandler) setPrototypeOf(target *Object, proto *Object) (bool, bool) { + if trap := h.handler.SetPrototypeOf; trap != nil { + return trap(target, proto), true + } + return false, false +} + +func (h *nativeProxyHandler) isExtensible(target *Object) (bool, bool) { + if trap := h.handler.IsExtensible; trap != nil { + return trap(target), true + } + return false, false +} + +func (h *nativeProxyHandler) preventExtensions(target *Object) (bool, bool) { + if trap := h.handler.PreventExtensions; trap != nil { + return trap(target), true + } + return false, false +} + +func (h *nativeProxyHandler) getOwnPropertyDescriptorStr(target *Object, prop unistring.String) (Value, bool) { + if trap := h.handler.GetOwnPropertyDescriptorIdx; trap != nil { + if idx, ok := strToInt(prop); ok { + desc := trap(target, idx) + return desc.toValue(target.runtime), true + } + } + if trap := h.handler.GetOwnPropertyDescriptor; trap != nil { + desc := trap(target, prop.String()) + return desc.toValue(target.runtime), true + } + return nil, false +} + +func (h *nativeProxyHandler) getOwnPropertyDescriptorIdx(target *Object, prop valueInt) (Value, bool) { + if trap := h.handler.GetOwnPropertyDescriptorIdx; trap != nil { + desc := trap(target, toIntStrict(int64(prop))) + return desc.toValue(target.runtime), true + } + if trap := h.handler.GetOwnPropertyDescriptor; trap != nil { + desc := trap(target, prop.String()) + return desc.toValue(target.runtime), true + } + return nil, false +} + +func (h *nativeProxyHandler) getOwnPropertyDescriptorSym(target *Object, prop *Symbol) (Value, bool) { + if trap := h.handler.GetOwnPropertyDescriptorSym; trap != nil { + desc := trap(target, prop) + return desc.toValue(target.runtime), true + } + return nil, false +} + +func (h *nativeProxyHandler) definePropertyStr(target *Object, prop unistring.String, desc PropertyDescriptor) (bool, bool) { + if trap := h.handler.DefinePropertyIdx; trap != nil { + if idx, ok := strToInt(prop); ok { + return trap(target, idx, desc), true + } + } + if trap := h.handler.DefineProperty; trap != nil { + return trap(target, prop.String(), desc), true + } + return false, false +} + +func (h *nativeProxyHandler) definePropertyIdx(target *Object, prop valueInt, desc PropertyDescriptor) (bool, bool) { + if trap := h.handler.DefinePropertyIdx; trap != nil { + return trap(target, toIntStrict(int64(prop)), desc), true + } + if trap := h.handler.DefineProperty; trap != nil { + return trap(target, prop.String(), desc), true + } + return false, false +} + +func (h *nativeProxyHandler) definePropertySym(target *Object, prop *Symbol, desc PropertyDescriptor) (bool, bool) { + if trap := h.handler.DefinePropertySym; trap != nil { + return trap(target, prop, desc), true + } + return false, false +} + +func (h *nativeProxyHandler) hasStr(target *Object, prop unistring.String) (bool, bool) { + if trap := h.handler.HasIdx; trap != nil { + if idx, ok := strToInt(prop); ok { + return trap(target, idx), true + } + } + if trap := h.handler.Has; trap != nil { + return trap(target, prop.String()), true + } + return false, false +} + +func (h *nativeProxyHandler) hasIdx(target *Object, prop valueInt) (bool, bool) { + if trap := h.handler.HasIdx; trap != nil { + return trap(target, toIntStrict(int64(prop))), true + } + if trap := h.handler.Has; trap != nil { + return trap(target, prop.String()), true + } + return false, false +} + +func (h *nativeProxyHandler) hasSym(target *Object, prop *Symbol) (bool, bool) { + if trap := h.handler.HasSym; trap != nil { + return trap(target, prop), true + } + return false, false +} + +func (h *nativeProxyHandler) getStr(target *Object, prop unistring.String, receiver Value) (Value, bool) { + if trap := h.handler.GetIdx; trap != nil { + if idx, ok := strToInt(prop); ok { + return trap(target, idx, receiver), true + } + } + if trap := h.handler.Get; trap != nil { + return trap(target, prop.String(), receiver), true + } + return nil, false +} + +func (h *nativeProxyHandler) getIdx(target *Object, prop valueInt, receiver Value) (Value, bool) { + if trap := h.handler.GetIdx; trap != nil { + return trap(target, toIntStrict(int64(prop)), receiver), true + } + if trap := h.handler.Get; trap != nil { + return trap(target, prop.String(), receiver), true + } + return nil, false +} + +func (h *nativeProxyHandler) getSym(target *Object, prop *Symbol, receiver Value) (Value, bool) { + if trap := h.handler.GetSym; trap != nil { + return trap(target, prop, receiver), true + } + return nil, false +} + +func (h *nativeProxyHandler) setStr(target *Object, prop unistring.String, value Value, receiver Value) (bool, bool) { + if trap := h.handler.SetIdx; trap != nil { + if idx, ok := strToInt(prop); ok { + return trap(target, idx, value, receiver), true + } + } + if trap := h.handler.Set; trap != nil { + return trap(target, prop.String(), value, receiver), true + } + return false, false +} + +func (h *nativeProxyHandler) setIdx(target *Object, prop valueInt, value Value, receiver Value) (bool, bool) { + if trap := h.handler.SetIdx; trap != nil { + return trap(target, toIntStrict(int64(prop)), value, receiver), true + } + if trap := h.handler.Set; trap != nil { + return trap(target, prop.String(), value, receiver), true + } + return false, false +} + +func (h *nativeProxyHandler) setSym(target *Object, prop *Symbol, value Value, receiver Value) (bool, bool) { + if trap := h.handler.SetSym; trap != nil { + return trap(target, prop, value, receiver), true + } + return false, false +} + +func (h *nativeProxyHandler) deleteStr(target *Object, prop unistring.String) (bool, bool) { + if trap := h.handler.DeletePropertyIdx; trap != nil { + if idx, ok := strToInt(prop); ok { + return trap(target, idx), true + } + } + if trap := h.handler.DeleteProperty; trap != nil { + return trap(target, prop.String()), true + } + return false, false +} + +func (h *nativeProxyHandler) deleteIdx(target *Object, prop valueInt) (bool, bool) { + if trap := h.handler.DeletePropertyIdx; trap != nil { + return trap(target, toIntStrict(int64(prop))), true + } + if trap := h.handler.DeleteProperty; trap != nil { + return trap(target, prop.String()), true + } + return false, false +} + +func (h *nativeProxyHandler) deleteSym(target *Object, prop *Symbol) (bool, bool) { + if trap := h.handler.DeletePropertySym; trap != nil { + return trap(target, prop), true + } + return false, false +} + +func (h *nativeProxyHandler) ownKeys(target *Object) (*Object, bool) { + if trap := h.handler.OwnKeys; trap != nil { + return trap(target), true + } + return nil, false +} + +func (h *nativeProxyHandler) apply(target *Object, this Value, args []Value) (Value, bool) { + if trap := h.handler.Apply; trap != nil { + return trap(target, this, args), true + } + return nil, false +} + +func (h *nativeProxyHandler) construct(target *Object, args []Value, newTarget *Object) (Value, bool) { + if trap := h.handler.Construct; trap != nil { + return trap(target, args, newTarget), true + } + return nil, false +} + +func (h *nativeProxyHandler) toObject(runtime *Runtime) *Object { + return runtime.ToValue(h.handler).ToObject(runtime) +} + +func (r *Runtime) newNativeProxyHandler(nativeHandler *ProxyTrapConfig) proxyHandler { + return &nativeProxyHandler{handler: nativeHandler} +} + +// ProxyTrapConfig provides a simplified Go-friendly API for implementing Proxy traps. +// If an *Idx trap is defined it gets called for integer property keys, including negative ones. Note that +// this only includes string property keys that represent a canonical integer +// (i.e. "0", "123", but not "00", "01", " 1" or "-0"). +// For efficiency strings representing integers exceeding 2^53 are not checked to see if they are canonical, +// i.e. the *Idx traps will receive "9007199254740993" as well as "9007199254740994", even though the former is not +// a canonical representation in ECMAScript (Number("9007199254740993") === 9007199254740992). +// See https://262.ecma-international.org/#sec-canonicalnumericindexstring +// If an *Idx trap is not set, the corresponding string one is used. +type ProxyTrapConfig struct { + // A trap for Object.getPrototypeOf, Reflect.getPrototypeOf, __proto__, Object.prototype.isPrototypeOf, instanceof + GetPrototypeOf func(target *Object) (prototype *Object) + + // A trap for Object.setPrototypeOf, Reflect.setPrototypeOf + SetPrototypeOf func(target *Object, prototype *Object) (success bool) + + // A trap for Object.isExtensible, Reflect.isExtensible + IsExtensible func(target *Object) (success bool) + + // A trap for Object.preventExtensions, Reflect.preventExtensions + PreventExtensions func(target *Object) (success bool) + + // A trap for Object.getOwnPropertyDescriptor, Reflect.getOwnPropertyDescriptor (string properties) + GetOwnPropertyDescriptor func(target *Object, prop string) (propertyDescriptor PropertyDescriptor) + + // A trap for Object.getOwnPropertyDescriptor, Reflect.getOwnPropertyDescriptor (integer properties) + GetOwnPropertyDescriptorIdx func(target *Object, prop int) (propertyDescriptor PropertyDescriptor) + + // A trap for Object.getOwnPropertyDescriptor, Reflect.getOwnPropertyDescriptor (Symbol properties) + GetOwnPropertyDescriptorSym func(target *Object, prop *Symbol) (propertyDescriptor PropertyDescriptor) + + // A trap for Object.defineProperty, Reflect.defineProperty (string properties) + DefineProperty func(target *Object, key string, propertyDescriptor PropertyDescriptor) (success bool) + + // A trap for Object.defineProperty, Reflect.defineProperty (integer properties) + DefinePropertyIdx func(target *Object, key int, propertyDescriptor PropertyDescriptor) (success bool) + + // A trap for Object.defineProperty, Reflect.defineProperty (Symbol properties) + DefinePropertySym func(target *Object, key *Symbol, propertyDescriptor PropertyDescriptor) (success bool) + + // A trap for the in operator, with operator, Reflect.has (string properties) + Has func(target *Object, property string) (available bool) + + // A trap for the in operator, with operator, Reflect.has (integer properties) + HasIdx func(target *Object, property int) (available bool) + + // A trap for the in operator, with operator, Reflect.has (Symbol properties) + HasSym func(target *Object, property *Symbol) (available bool) + + // A trap for getting property values, Reflect.get (string properties) + Get func(target *Object, property string, receiver Value) (value Value) + + // A trap for getting property values, Reflect.get (integer properties) + GetIdx func(target *Object, property int, receiver Value) (value Value) + + // A trap for getting property values, Reflect.get (Symbol properties) + GetSym func(target *Object, property *Symbol, receiver Value) (value Value) + + // A trap for setting property values, Reflect.set (string properties) + Set func(target *Object, property string, value Value, receiver Value) (success bool) + + // A trap for setting property values, Reflect.set (integer properties) + SetIdx func(target *Object, property int, value Value, receiver Value) (success bool) + + // A trap for setting property values, Reflect.set (Symbol properties) + SetSym func(target *Object, property *Symbol, value Value, receiver Value) (success bool) + + // A trap for the delete operator, Reflect.deleteProperty (string properties) + DeleteProperty func(target *Object, property string) (success bool) + + // A trap for the delete operator, Reflect.deleteProperty (integer properties) + DeletePropertyIdx func(target *Object, property int) (success bool) + + // A trap for the delete operator, Reflect.deleteProperty (Symbol properties) + DeletePropertySym func(target *Object, property *Symbol) (success bool) + + // A trap for Object.getOwnPropertyNames, Object.getOwnPropertySymbols, Object.keys, Reflect.ownKeys + OwnKeys func(target *Object) (object *Object) + + // A trap for a function call, Function.prototype.apply, Function.prototype.call, Reflect.apply + Apply func(target *Object, this Value, argumentsList []Value) (value Value) + + // A trap for the new operator, Reflect.construct + Construct func(target *Object, argumentsList []Value, newTarget *Object) (value *Object) +} + +func (r *Runtime) newProxy(args []Value, proto *Object) *Object { + if len(args) >= 2 { + if target, ok := args[0].(*Object); ok { + if proxyHandler, ok := args[1].(*Object); ok { + return r.newProxyObject(target, proxyHandler, proto).val + } + } + } + panic(r.NewTypeError("Cannot create proxy with a non-object as target or handler")) +} + +func (r *Runtime) builtin_newProxy(args []Value, newTarget *Object) *Object { + if newTarget == nil { + panic(r.needNew("Proxy")) + } + return r.newProxy(args, r.getPrototypeFromCtor(newTarget, r.getProxy(), r.global.ObjectPrototype)) +} + +func (r *Runtime) NewProxy(target *Object, nativeHandler *ProxyTrapConfig) Proxy { + if p, ok := target.self.(*proxyObject); ok { + if p.handler == nil { + panic(r.NewTypeError("Cannot create proxy with a revoked proxy as target")) + } + } + handler := r.newNativeProxyHandler(nativeHandler) + proxy := r._newProxyObject(target, handler, nil) + return Proxy{proxy: proxy} +} + +func (r *Runtime) builtin_proxy_revocable(call FunctionCall) Value { + if len(call.Arguments) >= 2 { + if target, ok := call.Argument(0).(*Object); ok { + if proxyHandler, ok := call.Argument(1).(*Object); ok { + proxy := r.newProxyObject(target, proxyHandler, nil) + revoke := r.newNativeFunc(func(FunctionCall) Value { + proxy.revoke() + return _undefined + }, "", 0) + ret := r.NewObject() + ret.self._putProp("proxy", proxy.val, true, true, true) + ret.self._putProp("revoke", revoke, true, true, true) + return ret + } + } + } + panic(r.NewTypeError("Cannot create proxy with a non-object as target or handler")) +} + +func (r *Runtime) createProxy(val *Object) objectImpl { + o := r.newNativeConstructOnly(val, r.builtin_newProxy, nil, "Proxy", 2) + + o._putProp("revocable", r.newNativeFunc(r.builtin_proxy_revocable, "revocable", 2), true, false, true) + return o +} + +func (r *Runtime) getProxy() *Object { + ret := r.global.Proxy + if ret == nil { + ret = &Object{runtime: r} + r.global.Proxy = ret + r.createProxy(ret) + } + return ret +} diff --git a/goja/builtin_proxy_test.go b/goja/builtin_proxy_test.go new file mode 100644 index 0000000..64ccafd --- /dev/null +++ b/goja/builtin_proxy_test.go @@ -0,0 +1,1275 @@ +package goja + +import ( + "strconv" + "testing" +) + +func TestProxy_Object_target_getPrototypeOf(t *testing.T) { + const SCRIPT = ` + var proto = {}; + var obj = Object.create(proto); + var proxy = new Proxy(obj, {}); + var p = Object.getPrototypeOf(proxy); + assert.sameValue(proto, p); + ` + + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestProxy_Object_proxy_getPrototypeOf(t *testing.T) { + const SCRIPT = ` + var proto = {}; + var proto2 = {}; + var obj = Object.create(proto); + var proxy = new Proxy(obj, { + getPrototypeOf: function(target) { + return proto2; + } + }); + var p = Object.getPrototypeOf(proxy); + assert.sameValue(proto2, p); + ` + + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestProxy_Object_native_proxy_getPrototypeOf(t *testing.T) { + const SCRIPT = ` + var p = Object.getPrototypeOf(proxy); + assert.sameValue(proto, p); + ` + + runtime := New() + + prototype := runtime.NewObject() + runtime.Set("proto", prototype) + + target := runtime.NewObject() + proxy := runtime.NewProxy(target, &ProxyTrapConfig{ + GetPrototypeOf: func(target *Object) *Object { + return prototype + }, + }) + runtime.Set("proxy", proxy) + + runtime.testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestProxy_Object_target_setPrototypeOf(t *testing.T) { + const SCRIPT = ` + var proto = {}; + var obj = {}; + Object.setPrototypeOf(obj, proto); + var proxy = new Proxy(obj, {}); + var p = Object.getPrototypeOf(proxy); + assert.sameValue(proto, p); + ` + + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestProxy_Object_proxy_setPrototypeOf(t *testing.T) { + const SCRIPT = ` + var proto = {}; + var proto2 = {}; + var obj = {}; + Object.setPrototypeOf(obj, proto); + var proxy = new Proxy(obj, { + setPrototypeOf: function(target, prototype) { + return Object.setPrototypeOf(target, proto2); + } + }); + Object.setPrototypeOf(proxy, null); + var p = Object.getPrototypeOf(proxy); + assert.sameValue(proto2, p); + ` + + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestProxy_Object_target_isExtensible(t *testing.T) { + const SCRIPT = ` + var obj = {}; + Object.seal(obj); + var proxy = new Proxy(obj, {}); + Object.isExtensible(proxy); + ` + + testScript(SCRIPT, valueFalse, t) +} + +func TestProxy_proxy_isExtensible(t *testing.T) { + const SCRIPT = ` + var obj = {}; + Object.seal(obj); + var proxy = new Proxy(obj, { + isExtensible: function(target) { + return false; + } + }); + Object.isExtensible(proxy); + ` + + testScript(SCRIPT, valueFalse, t) +} + +func TestProxy_native_proxy_isExtensible(t *testing.T) { + const SCRIPT = ` + (function() { + Object.preventExtensions(target); + return Object.isExtensible(proxy); + })(); + ` + + runtime := New() + + target := runtime.NewObject() + runtime.Set("target", target) + + proxy := runtime.NewProxy(target, &ProxyTrapConfig{ + IsExtensible: func(target *Object) (success bool) { + return false + }, + }) + runtime.Set("proxy", proxy) + + val, err := runtime.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + if val.ToBoolean() { + t.Fatal() + } +} + +func TestProxy_Object_target_preventExtensions(t *testing.T) { + const SCRIPT = ` + var obj = { + canEvolve: true + }; + var proxy = new Proxy(obj, {}); + Object.preventExtensions(proxy); + proxy.canEvolve + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestProxy_proxy_preventExtensions(t *testing.T) { + const SCRIPT = ` + var obj = { + canEvolve: true + }; + var proxy = new Proxy(obj, { + preventExtensions: function(target) { + target.canEvolve = false; + Object.preventExtensions(obj); + return true; + } + }); + Object.preventExtensions(proxy); + proxy.canEvolve; + ` + + testScript(SCRIPT, valueFalse, t) +} + +func TestProxy_native_proxy_preventExtensions(t *testing.T) { + const SCRIPT = ` + (function() { + Object.preventExtensions(proxy); + return proxy.canEvolve; + })(); + ` + + runtime := New() + + target := runtime.NewObject() + target.Set("canEvolve", true) + runtime.Set("target", target) + + proxy := runtime.NewProxy(target, &ProxyTrapConfig{ + PreventExtensions: func(target *Object) (success bool) { + target.Set("canEvolve", false) + _, err := runtime.RunString("Object.preventExtensions(target)") + if err != nil { + panic(err) + } + return true + }, + }) + runtime.Set("proxy", proxy) + + val, err := runtime.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + if val.ToBoolean() { + t.Fatal() + } +} + +func TestProxy_Object_target_getOwnPropertyDescriptor(t *testing.T) { + const SCRIPT = ` + var desc = { + configurable: false, + enumerable: false, + value: 42, + writable: false + }; + + var obj = {}; + Object.defineProperty(obj, "foo", desc); + + var proxy = new Proxy(obj, {}); + + var desc2 = Object.getOwnPropertyDescriptor(proxy, "foo"); + desc2.value + ` + + testScript(SCRIPT, valueInt(42), t) +} + +func TestProxy_proxy_getOwnPropertyDescriptor(t *testing.T) { + const SCRIPT = ` + var desc = { + configurable: false, + enumerable: false, + value: 42, + writable: false + }; + var proxy_desc = { + configurable: false, + enumerable: false, + value: 24, + writable: false + }; + + var obj = {}; + Object.defineProperty(obj, "foo", desc); + + var proxy = new Proxy(obj, { + getOwnPropertyDescriptor: function(target, property) { + return proxy_desc; + } + }); + + assert.throws(TypeError, function() { + Object.getOwnPropertyDescriptor(proxy, "foo"); + }); + undefined; + ` + + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestProxy_native_proxy_getOwnPropertyDescriptor(t *testing.T) { + const SCRIPT = ` + (function() { + var desc = { + configurable: true, + enumerable: false, + value: 42, + writable: false + }; + var proxy_desc = { + configurable: true, + enumerable: false, + value: 24, + writable: false + }; + + var obj = {}; + Object.defineProperty(obj, "foo", desc); + + return function(constructor) { + var proxy = constructor(obj, proxy_desc); + + var desc2 = Object.getOwnPropertyDescriptor(proxy, "foo"); + return desc2.value + } + })(); + ` + + runtime := New() + + constructor := func(call FunctionCall) Value { + target := call.Argument(0).(*Object) + proxyDesc := call.Argument(1).(*Object) + + return runtime.NewProxy(target, &ProxyTrapConfig{ + GetOwnPropertyDescriptor: func(target *Object, prop string) PropertyDescriptor { + return runtime.toPropertyDescriptor(proxyDesc) + }, + }).proxy.val + } + + val, err := runtime.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if c, ok := val.(*Object).self.assertCallable(); ok { + val := c(FunctionCall{ + This: val, + Arguments: []Value{runtime.ToValue(constructor)}, + }) + if i := val.ToInteger(); i != 24 { + t.Fatalf("val: %d", i) + } + } else { + t.Fatal("not a function") + } +} + +func TestProxy_native_proxy_getOwnPropertyDescriptorIdx(t *testing.T) { + vm := New() + a := vm.NewArray() + proxy1 := vm.NewProxy(a, &ProxyTrapConfig{ + GetOwnPropertyDescriptor: func(target *Object, prop string) PropertyDescriptor { + panic(vm.NewTypeError("GetOwnPropertyDescriptor was called for %q", prop)) + }, + GetOwnPropertyDescriptorIdx: func(target *Object, prop int) PropertyDescriptor { + if prop >= -1 && prop <= 1 { + return PropertyDescriptor{ + Value: vm.ToValue(prop), + Configurable: FLAG_TRUE, + } + } + return PropertyDescriptor{} + }, + }) + + proxy2 := vm.NewProxy(a, &ProxyTrapConfig{ + GetOwnPropertyDescriptor: func(target *Object, prop string) PropertyDescriptor { + switch prop { + case "-1", "0", "1": + return PropertyDescriptor{ + Value: vm.ToValue(prop), + Configurable: FLAG_TRUE, + } + } + return PropertyDescriptor{} + }, + }) + + proxy3 := vm.NewProxy(a, &ProxyTrapConfig{ + GetOwnPropertyDescriptor: func(target *Object, prop string) PropertyDescriptor { + return PropertyDescriptor{ + Value: vm.ToValue(prop), + Configurable: FLAG_TRUE, + } + }, + GetOwnPropertyDescriptorIdx: func(target *Object, prop int) PropertyDescriptor { + panic(vm.NewTypeError("GetOwnPropertyDescriptorIdx was called for %d", prop)) + }, + }) + + vm.Set("proxy1", proxy1) + vm.Set("proxy2", proxy2) + vm.Set("proxy3", proxy3) + vm.testScriptWithTestLibX(` + var desc; + for (var i = -1; i <= 1; i++) { + desc = Object.getOwnPropertyDescriptor(proxy1, i); + assert(deepEqual(desc, {value: i, writable: false, enumerable: false, configurable: true}), "1. int "+i); + + desc = Object.getOwnPropertyDescriptor(proxy1, ""+i); + assert(deepEqual(desc, {value: i, writable: false, enumerable: false, configurable: true}), "1. str "+i); + + desc = Object.getOwnPropertyDescriptor(proxy2, i); + assert(deepEqual(desc, {value: ""+i, writable: false, enumerable: false, configurable: true}), "2. int "+i); + + desc = Object.getOwnPropertyDescriptor(proxy2, ""+i); + assert(deepEqual(desc, {value: ""+i, writable: false, enumerable: false, configurable: true}), "2. str "+i); + } + + for (const prop of ["00", " 0", "-0", "01"]) { + desc = Object.getOwnPropertyDescriptor(proxy3, prop); + assert(deepEqual(desc, {value: prop, writable: false, enumerable: false, configurable: true}), "3. "+prop); + } + `, _undefined, t) +} + +func TestProxy_native_proxy_getOwnPropertyDescriptorSym(t *testing.T) { + vm := New() + o := vm.NewObject() + sym := NewSymbol("42") + vm.Set("sym", sym) + proxy := vm.NewProxy(o, &ProxyTrapConfig{ + GetOwnPropertyDescriptorSym: func(target *Object, s *Symbol) PropertyDescriptor { + if target != o { + panic(vm.NewTypeError("Invalid target")) + } + if s == sym { + return PropertyDescriptor{ + Value: vm.ToValue("passed"), + Writable: FLAG_TRUE, + Configurable: FLAG_TRUE, + } + } + return PropertyDescriptor{} + }, + }) + + vm.Set("proxy", proxy) + vm.testScriptWithTestLibX(` + var desc = Object.getOwnPropertyDescriptor(proxy, sym); + assert(deepEqual(desc, {value: "passed", writable: true, enumerable: false, configurable: true})); + assert.sameValue(Object.getOwnPropertyDescriptor(proxy, Symbol.iterator), undefined); + `, _undefined, t) +} + +func TestProxy_native_proxy_getOwnPropertyDescriptor_non_existing(t *testing.T) { + vm := New() + proxy := vm.NewProxy(vm.NewObject(), &ProxyTrapConfig{ + GetOwnPropertyDescriptor: func(target *Object, prop string) (propertyDescriptor PropertyDescriptor) { + return // empty PropertyDescriptor + }, + }) + vm.Set("proxy", proxy) + res, err := vm.RunString(`Object.getOwnPropertyDescriptor(proxy, "foo") === undefined`) + if err != nil { + t.Fatal(err) + } + if res != valueTrue { + t.Fatal(res) + } +} + +func TestProxy_Object_target_defineProperty(t *testing.T) { + const SCRIPT = ` + var obj = {}; + var proxy = new Proxy(obj, {}); + Object.defineProperty(proxy, "foo", { + value: "test123" + }); + proxy.foo; + ` + + testScript(SCRIPT, asciiString("test123"), t) +} + +func TestProxy_proxy_defineProperty(t *testing.T) { + const SCRIPT = ` + var obj = {}; + var proxy = new Proxy(obj, { + defineProperty: function(target, prop, descriptor) { + target.foo = "321tset"; + return true; + } + }); + Object.defineProperty(proxy, "foo", { + value: "test123" + }); + proxy.foo; + ` + + testScript(SCRIPT, asciiString("321tset"), t) +} + +func TestProxy_native_proxy_defineProperty(t *testing.T) { + const SCRIPT = ` + Object.defineProperty(proxy, "foo", { + value: "teststr" + }); + Object.defineProperty(proxy, "0", { + value: "testidx" + }); + Object.defineProperty(proxy, Symbol.toStringTag, { + value: "testsym" + }); + assert.sameValue(proxy.foo, "teststr-passed-str"); + assert.sameValue(proxy[0], "testidx-passed-idx"); + assert.sameValue(proxy[Symbol.toStringTag], "testsym-passed-sym"); + ` + + runtime := New() + + target := runtime.NewObject() + + proxy := runtime.NewProxy(target, &ProxyTrapConfig{ + DefineProperty: func(target *Object, key string, propertyDescriptor PropertyDescriptor) (success bool) { + target.Set(key, propertyDescriptor.Value.String()+"-passed-str") + return true + }, + DefinePropertyIdx: func(target *Object, key int, propertyDescriptor PropertyDescriptor) (success bool) { + target.Set(strconv.Itoa(key), propertyDescriptor.Value.String()+"-passed-idx") + return true + }, + DefinePropertySym: func(target *Object, key *Symbol, propertyDescriptor PropertyDescriptor) (success bool) { + target.SetSymbol(key, propertyDescriptor.Value.String()+"-passed-sym") + return true + }, + }) + runtime.Set("proxy", proxy) + + runtime.testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestProxy_target_has_in(t *testing.T) { + const SCRIPT = ` + var obj = { + secret: true + }; + var proxy = new Proxy(obj, {}); + + "secret" in proxy + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestProxy_proxy_has_in(t *testing.T) { + const SCRIPT = ` + var obj = { + secret: true + }; + var proxy = new Proxy(obj, { + has: function(target, key) { + return key !== "secret"; + } + }); + + "secret" in proxy + ` + + testScript(SCRIPT, valueFalse, t) +} + +func TestProxy_target_has_with(t *testing.T) { + const SCRIPT = ` + var obj = { + secret: true + }; + var proxy = new Proxy(obj, {}); + + with(proxy) { + (secret); + } + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestProxy_proxy_has_with(t *testing.T) { + const SCRIPT = ` + var obj = { + secret: true + }; + var proxy = new Proxy(obj, { + has: function(target, key) { + return key !== "secret"; + } + }); + + var thrown = false; + try { + with(proxy) { + (secret); + } + } catch (e) { + if (e instanceof ReferenceError) { + thrown = true; + } else { + throw e; + } + } + thrown; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestProxy_target_get(t *testing.T) { + const SCRIPT = ` + var obj = {}; + var proxy = new Proxy(obj, {}); + Object.defineProperty(proxy, "foo", { + value: "test123" + }); + proxy.foo; + ` + + testScript(SCRIPT, asciiString("test123"), t) +} + +func TestProxy_proxy_get(t *testing.T) { + const SCRIPT = ` + var obj = {}; + var proxy = new Proxy(obj, { + get: function(target, prop, receiver) { + return "321tset" + } + }); + Object.defineProperty(proxy, "foo", { + value: "test123", + configurable: true, + }); + proxy.foo; + ` + + testScript(SCRIPT, asciiString("321tset"), t) +} + +func TestProxy_proxy_get_json_stringify(t *testing.T) { + const SCRIPT = ` + var obj = {}; + var propValue = "321tset"; + var _handler, _target, _prop, _receiver; + var proxy = new Proxy(obj, { + ownKeys: function() { + return ["foo"]; + }, + getOwnPropertyDescriptor: function(target, prop) { + if (prop === "foo") { + return { + value: propValue, + enumerable: true, + configurable: true + } + } + }, + get: function(target, prop, receiver) { + if (prop === "foo") { + _prop = prop; + _receiver = receiver; + return propValue; + } + return obj[prop]; + } + }); + var res = JSON.stringify(proxy); + assert.sameValue(res, '{"foo":"321tset"}'); + assert.sameValue(_prop, "foo"); + assert.sameValue(_receiver, proxy); + ` + + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestProxy_native_proxy_get(t *testing.T) { + vm := New() + propValueStr := vm.ToValue("321tset") + propValueIdx := vm.ToValue("idx") + propValueSym := vm.ToValue("sym") + sym := NewSymbol("test") + obj := vm.NewObject() + proxy := vm.NewProxy(obj, &ProxyTrapConfig{ + OwnKeys: func(*Object) *Object { + return vm.NewArray("0", "foo") + }, + GetOwnPropertyDescriptor: func(target *Object, prop string) (propertyDescriptor PropertyDescriptor) { + if prop == "foo" { + return PropertyDescriptor{ + Value: propValueStr, + Enumerable: FLAG_TRUE, + Configurable: FLAG_TRUE, + } + } + if prop == "0" { + panic(vm.NewTypeError("GetOwnPropertyDescriptor(0) was called")) + } + return + }, + GetOwnPropertyDescriptorIdx: func(target *Object, prop int) (propertyDescriptor PropertyDescriptor) { + if prop == 0 { + return PropertyDescriptor{ + Value: propValueIdx, + Enumerable: FLAG_TRUE, + Configurable: FLAG_TRUE, + } + } + return + }, + Get: func(target *Object, property string, receiver Value) (value Value) { + if property == "foo" { + return propValueStr + } + if property == "0" { + panic(vm.NewTypeError("Get(0) was called")) + } + return obj.Get(property) + }, + GetIdx: func(target *Object, property int, receiver Value) (value Value) { + if property == 0 { + return propValueIdx + } + return obj.Get(strconv.Itoa(property)) + }, + GetSym: func(target *Object, property *Symbol, receiver Value) (value Value) { + if property == sym { + return propValueSym + } + return obj.GetSymbol(property) + }, + }) + vm.Set("proxy", proxy) + res, err := vm.RunString(`JSON.stringify(proxy)`) + if err != nil { + t.Fatal(err) + } + if !res.SameAs(asciiString(`{"0":"idx","foo":"321tset"}`)) { + t.Fatalf("res: %v", res) + } + res, err = vm.RunString(`proxy[Symbol.toPrimitive]`) + if err != nil { + t.Fatal(err) + } + if !IsUndefined(res) { + t.Fatalf("res: %v", res) + } + + res, err = vm.RunString(`proxy.hasOwnProperty(Symbol.toPrimitive)`) + if err != nil { + t.Fatal(err) + } + if !res.SameAs(valueFalse) { + t.Fatalf("res: %v", res) + } + + if val := vm.ToValue(proxy).(*Object).GetSymbol(sym); val == nil || !val.SameAs(propValueSym) { + t.Fatalf("Get(symbol): %v", val) + } + + res, err = vm.RunString(`proxy.toString()`) + if err != nil { + t.Fatal(err) + } + if !res.SameAs(asciiString(`[object Object]`)) { + t.Fatalf("res: %v", res) + } +} + +func TestProxy_native_proxy_set(t *testing.T) { + vm := New() + propValueStr := vm.ToValue("321tset") + propValueIdx := vm.ToValue("idx") + propValueSym := vm.ToValue("sym") + sym := NewSymbol("test") + obj := vm.NewObject() + proxy := vm.NewProxy(obj, &ProxyTrapConfig{ + Set: func(target *Object, property string, value Value, receiver Value) (success bool) { + if property == "str" { + obj.Set(property, propValueStr) + return true + } + panic(vm.NewTypeError("Setter for unexpected property: %q", property)) + }, + SetIdx: func(target *Object, property int, value Value, receiver Value) (success bool) { + if property == 0 { + obj.Set(strconv.Itoa(property), propValueIdx) + return true + } + panic(vm.NewTypeError("Setter for unexpected idx property: %d", property)) + }, + SetSym: func(target *Object, property *Symbol, value Value, receiver Value) (success bool) { + if property == sym { + obj.SetSymbol(property, propValueSym) + return true + } + panic(vm.NewTypeError("Setter for unexpected sym property: %q", property.String())) + }, + }) + proxyObj := vm.ToValue(proxy).ToObject(vm) + err := proxyObj.Set("str", "") + if err != nil { + t.Fatal(err) + } + err = proxyObj.Set("0", "") + if err != nil { + t.Fatal(err) + } + err = proxyObj.SetSymbol(sym, "") + if err != nil { + t.Fatal(err) + } + if v := obj.Get("str"); !propValueStr.SameAs(v) { + t.Fatal(v) + } + if v := obj.Get("0"); !propValueIdx.SameAs(v) { + t.Fatal(v) + } + if v := obj.GetSymbol(sym); !propValueSym.SameAs(v) { + t.Fatal(v) + } +} + +func TestProxy_target_set_prop(t *testing.T) { + const SCRIPT = ` + var obj = {}; + var proxy = new Proxy(obj, {}); + proxy.foo = "test123"; + proxy.foo; + ` + + testScript(SCRIPT, asciiString("test123"), t) +} + +func TestProxy_proxy_set_prop(t *testing.T) { + const SCRIPT = ` + var obj = {}; + var proxy = new Proxy(obj, { + set: function(target, prop, receiver) { + target.foo = "321tset"; + return true; + } + }); + proxy.foo = "test123"; + proxy.foo; + ` + + testScript(SCRIPT, asciiString("321tset"), t) +} +func TestProxy_target_set_associative(t *testing.T) { + const SCRIPT = ` + var obj = {}; + var proxy = new Proxy(obj, {}); + proxy["foo"] = "test123"; + proxy.foo; + ` + + testScript(SCRIPT, asciiString("test123"), t) +} + +func TestProxy_proxy_set_associative(t *testing.T) { + const SCRIPT = ` + var obj = {}; + var proxy = new Proxy(obj, { + set: function(target, property, value, receiver) { + target["foo"] = "321tset"; + return true; + } + }); + proxy["foo"] = "test123"; + proxy.foo; + ` + + testScript(SCRIPT, asciiString("321tset"), t) +} + +func TestProxy_target_delete(t *testing.T) { + const SCRIPT = ` + var obj = { + foo: "test" + }; + var proxy = new Proxy(obj, {}); + delete proxy.foo; + + proxy.foo; + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestProxy_proxy_delete(t *testing.T) { + const SCRIPT = ` + var obj = { + foo: "test" + }; + var proxy = new Proxy(obj, { + deleteProperty: function(target, prop) { + return true; + } + }); + delete proxy.foo; + + proxy.foo; + ` + + testScript(SCRIPT, asciiString("test"), t) +} + +func TestProxy_native_delete(t *testing.T) { + vm := New() + sym := NewSymbol("test") + obj := vm.NewObject() + var strCalled, idxCalled, symCalled, strNegCalled, idxNegCalled, symNegCalled bool + proxy := vm.NewProxy(obj, &ProxyTrapConfig{ + DeleteProperty: func(target *Object, property string) (success bool) { + if property == "str" { + strCalled = true + return true + } + if property == "strNeg" { + strNegCalled = true + return false + } + panic(vm.NewTypeError("DeleteProperty for unexpected property: %q", property)) + }, + DeletePropertyIdx: func(target *Object, property int) (success bool) { + if property == 0 { + idxCalled = true + return true + } + if property == 1 { + idxNegCalled = true + return false + } + panic(vm.NewTypeError("DeletePropertyIdx for unexpected idx property: %d", property)) + }, + DeletePropertySym: func(target *Object, property *Symbol) (success bool) { + if property == sym { + symCalled = true + return true + } + if property == SymIterator { + symNegCalled = true + return false + } + panic(vm.NewTypeError("DeletePropertySym for unexpected sym property: %q", property.String())) + }, + }) + proxyObj := vm.ToValue(proxy).ToObject(vm) + err := proxyObj.Delete("str") + if err != nil { + t.Fatal(err) + } + err = proxyObj.Delete("0") + if err != nil { + t.Fatal(err) + } + err = proxyObj.DeleteSymbol(sym) + if err != nil { + t.Fatal(err) + } + if !strCalled { + t.Fatal("str") + } + if !idxCalled { + t.Fatal("idx") + } + if !symCalled { + t.Fatal("sym") + } + vm.Set("proxy", proxy) + _, err = vm.RunString(` + if (delete proxy.strNeg) { + throw new Error("strNeg"); + } + if (delete proxy[1]) { + throw new Error("idxNeg"); + } + if (delete proxy[Symbol.iterator]) { + throw new Error("symNeg"); + } + `) + if err != nil { + t.Fatal(err) + } + if !strNegCalled { + t.Fatal("strNeg") + } + if !idxNegCalled { + t.Fatal("idxNeg") + } + if !symNegCalled { + t.Fatal("symNeg") + } +} + +func TestProxy_target_keys(t *testing.T) { + const SCRIPT = ` + var obj = { + foo: "test" + }; + var proxy = new Proxy(obj, {}); + + var keys = Object.keys(proxy); + if (keys.length != 1) { + throw new Error("assertion error"); + } + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestProxy_proxy_keys(t *testing.T) { + const SCRIPT = ` + var obj = { + foo: "test" + }; + var proxy = new Proxy(obj, { + ownKeys: function(target) { + return ["foo", "bar"]; + } + }); + + var keys = Object.keys(proxy); + if (keys.length !== 1) { + throw new Error("length is "+keys.length); + } + if (keys[0] !== "foo") { + throw new Error("keys[0] is "+keys[0]); + } + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestProxy_target_call(t *testing.T) { + const SCRIPT = ` + var obj = function() { + return "test" + } + + var proxy = new Proxy(obj, {}); + + proxy(); + ` + + testScript(SCRIPT, asciiString("test"), t) +} + +func TestProxy_proxy_call(t *testing.T) { + const SCRIPT = ` + var obj = function() { + return "test" + } + + var proxy = new Proxy(obj, { + apply: function(target, thisArg, args) { + return "tset" + } + }); + + proxy(); + ` + + testScript(SCRIPT, asciiString("tset"), t) +} + +func TestProxy_target_func_apply(t *testing.T) { + const SCRIPT = ` + var obj = function() { + return "test" + } + + var proxy = new Proxy(obj, {}); + + proxy.apply(); + ` + + testScript(SCRIPT, asciiString("test"), t) +} + +func TestProxy_proxy_func_apply(t *testing.T) { + const SCRIPT = ` + var obj = function() { + return "test" + } + + var proxy = new Proxy(obj, { + apply: function(target, thisArg, args) { + return "tset" + } + }); + + proxy.apply(); + ` + + testScript(SCRIPT, asciiString("tset"), t) +} + +func TestProxy_target_func_call(t *testing.T) { + const SCRIPT = ` + var obj = function() { + return "test" + } + + var proxy = new Proxy(obj, {}); + + proxy.call(); + ` + + testScript(SCRIPT, asciiString("test"), t) +} + +func TestProxy_proxy_func_call(t *testing.T) { + const SCRIPT = ` + var obj = function() { + return "test" + } + + var proxy = new Proxy(obj, { + apply: function(target, thisArg, args) { + return "tset" + } + }); + + proxy.call(); + ` + + testScript(SCRIPT, asciiString("tset"), t) +} + +func TestProxy_target_new(t *testing.T) { + const SCRIPT = ` + var obj = function(word) { + this.foo = function() { + return word; + } + } + + var proxy = new Proxy(obj, {}); + + var instance = new proxy("test"); + instance.foo(); + ` + + testScript(SCRIPT, asciiString("test"), t) +} + +func TestProxy_proxy_new(t *testing.T) { + const SCRIPT = ` + var obj = function(word) { + this.foo = function() { + return word; + } + } + + var proxy = new Proxy(obj, { + construct: function(target, args, newTarget) { + var word = args[0]; + return { + foo: function() { + return "caught-" + word + } + } + } + }); + + var instance = new proxy("test"); + instance.foo(); + ` + + testScript(SCRIPT, asciiString("caught-test"), t) +} + +func TestProxy_Object_native_proxy_ownKeys(t *testing.T) { + headers := map[string][]string{ + "k0": {}, + } + vm := New() + proxy := vm.NewProxy(vm.NewObject(), &ProxyTrapConfig{ + OwnKeys: func(target *Object) (object *Object) { + keys := make([]interface{}, 0, len(headers)) + for k := range headers { + keys = append(keys, k) + } + return vm.ToValue(keys).ToObject(vm) + }, + GetOwnPropertyDescriptor: func(target *Object, prop string) PropertyDescriptor { + v, exists := headers[prop] + if exists { + return PropertyDescriptor{ + Value: vm.ToValue(v), + Enumerable: FLAG_TRUE, + Configurable: FLAG_TRUE, + } + } + return PropertyDescriptor{} + }, + }) + vm.Set("headers", proxy) + v, err := vm.RunString(` + var keys = Object.keys(headers); + keys.length === 1 && keys[0] === "k0"; + `) + if err != nil { + t.Fatal(err) + } + if v != valueTrue { + t.Fatal("not true", v) + } +} + +func TestProxy_proxy_forIn(t *testing.T) { + const SCRIPT = ` + var proto = { + a: 2, + protoProp: 1 + } + Object.defineProperty(proto, "protoNonEnum", { + value: 2, + writable: true, + configurable: true + }); + var target = Object.create(proto); + var proxy = new Proxy(target, { + ownKeys: function() { + return ["a", "b"]; + }, + getOwnPropertyDescriptor: function(target, p) { + switch (p) { + case "a": + case "b": + return { + value: 42, + enumerable: true, + configurable: true + } + } + }, + }); + + var forInResult = []; + for (var key in proxy) { + if (forInResult.indexOf(key) !== -1) { + throw new Error("Duplicate property "+key); + } + forInResult.push(key); + } + forInResult.length === 3 && forInResult[0] === "a" && forInResult[1] === "b" && forInResult[2] === "protoProp"; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestProxyExport(t *testing.T) { + vm := New() + v, err := vm.RunString(` + new Proxy({}, {}); + `) + if err != nil { + t.Fatal(err) + } + v1 := v.Export() + if _, ok := v1.(Proxy); !ok { + t.Fatalf("Export returned unexpected type: %T", v1) + } +} + +func TestProxy_proxy_createTargetNotCallable(t *testing.T) { + // from https://github.com/tc39/test262/blob/main/test/built-ins/Proxy/create-target-is-not-callable.js + const SCRIPT = ` + var p = new Proxy({}, {}); + + assert.throws(TypeError, function() { + p(); + }); + ` + + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestProxyEnumerableSymbols(t *testing.T) { + const SCRIPT = ` + var getOwnKeys = []; + var ownKeysResult = [Symbol(), "foo", "0"]; + var proxy = new Proxy({}, { + getOwnPropertyDescriptor: function(_target, key) { + getOwnKeys.push(key); + }, + ownKeys: function() { + return ownKeysResult; + }, + }); + + let {...$} = proxy; + compareArray(getOwnKeys, ownKeysResult); + ` + + testScriptWithTestLib(SCRIPT, valueTrue, t) +} diff --git a/goja/builtin_reflect.go b/goja/builtin_reflect.go new file mode 100644 index 0000000..17bb11a --- /dev/null +++ b/goja/builtin_reflect.go @@ -0,0 +1,140 @@ +package goja + +func (r *Runtime) builtin_reflect_apply(call FunctionCall) Value { + return r.toCallable(call.Argument(0))(FunctionCall{ + This: call.Argument(1), + Arguments: r.createListFromArrayLike(call.Argument(2))}) +} + +func (r *Runtime) toConstructor(v Value) func(args []Value, newTarget *Object) *Object { + if ctor := r.toObject(v).self.assertConstructor(); ctor != nil { + return ctor + } + panic(r.NewTypeError("Value is not a constructor")) +} + +func (r *Runtime) builtin_reflect_construct(call FunctionCall) Value { + target := call.Argument(0) + ctor := r.toConstructor(target) + var newTarget Value + if len(call.Arguments) > 2 { + newTarget = call.Argument(2) + r.toConstructor(newTarget) + } else { + newTarget = target + } + return ctor(r.createListFromArrayLike(call.Argument(1)), r.toObject(newTarget)) +} + +func (r *Runtime) builtin_reflect_defineProperty(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + key := toPropertyKey(call.Argument(1)) + desc := r.toPropertyDescriptor(call.Argument(2)) + + return r.toBoolean(target.defineOwnProperty(key, desc, false)) +} + +func (r *Runtime) builtin_reflect_deleteProperty(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + key := toPropertyKey(call.Argument(1)) + + return r.toBoolean(target.delete(key, false)) +} + +func (r *Runtime) builtin_reflect_get(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + key := toPropertyKey(call.Argument(1)) + var receiver Value + if len(call.Arguments) > 2 { + receiver = call.Arguments[2] + } + return target.get(key, receiver) +} + +func (r *Runtime) builtin_reflect_getOwnPropertyDescriptor(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + key := toPropertyKey(call.Argument(1)) + return r.valuePropToDescriptorObject(target.getOwnProp(key)) +} + +func (r *Runtime) builtin_reflect_getPrototypeOf(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + if proto := target.self.proto(); proto != nil { + return proto + } + + return _null +} + +func (r *Runtime) builtin_reflect_has(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + key := toPropertyKey(call.Argument(1)) + return r.toBoolean(target.hasProperty(key)) +} + +func (r *Runtime) builtin_reflect_isExtensible(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + return r.toBoolean(target.self.isExtensible()) +} + +func (r *Runtime) builtin_reflect_ownKeys(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + return r.newArrayValues(target.self.keys(true, nil)) +} + +func (r *Runtime) builtin_reflect_preventExtensions(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + return r.toBoolean(target.self.preventExtensions(false)) +} + +func (r *Runtime) builtin_reflect_set(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + var receiver Value + if len(call.Arguments) >= 4 { + receiver = call.Argument(3) + } else { + receiver = target + } + return r.toBoolean(target.set(call.Argument(1), call.Argument(2), receiver, false)) +} + +func (r *Runtime) builtin_reflect_setPrototypeOf(call FunctionCall) Value { + target := r.toObject(call.Argument(0)) + var proto *Object + if arg := call.Argument(1); arg != _null { + proto = r.toObject(arg) + } + return r.toBoolean(target.self.setProto(proto, false)) +} + +func (r *Runtime) createReflect(val *Object) objectImpl { + o := newBaseObjectObj(val, r.global.ObjectPrototype, classObject) + + o._putProp("apply", r.newNativeFunc(r.builtin_reflect_apply, "apply", 3), true, false, true) + o._putProp("construct", r.newNativeFunc(r.builtin_reflect_construct, "construct", 2), true, false, true) + o._putProp("defineProperty", r.newNativeFunc(r.builtin_reflect_defineProperty, "defineProperty", 3), true, false, true) + o._putProp("deleteProperty", r.newNativeFunc(r.builtin_reflect_deleteProperty, "deleteProperty", 2), true, false, true) + o._putProp("get", r.newNativeFunc(r.builtin_reflect_get, "get", 2), true, false, true) + o._putProp("getOwnPropertyDescriptor", r.newNativeFunc(r.builtin_reflect_getOwnPropertyDescriptor, "getOwnPropertyDescriptor", 2), true, false, true) + o._putProp("getPrototypeOf", r.newNativeFunc(r.builtin_reflect_getPrototypeOf, "getPrototypeOf", 1), true, false, true) + o._putProp("has", r.newNativeFunc(r.builtin_reflect_has, "has", 2), true, false, true) + o._putProp("isExtensible", r.newNativeFunc(r.builtin_reflect_isExtensible, "isExtensible", 1), true, false, true) + o._putProp("ownKeys", r.newNativeFunc(r.builtin_reflect_ownKeys, "ownKeys", 1), true, false, true) + o._putProp("preventExtensions", r.newNativeFunc(r.builtin_reflect_preventExtensions, "preventExtensions", 1), true, false, true) + o._putProp("set", r.newNativeFunc(r.builtin_reflect_set, "set", 3), true, false, true) + o._putProp("setPrototypeOf", r.newNativeFunc(r.builtin_reflect_setPrototypeOf, "setPrototypeOf", 2), true, false, true) + + o._putSym(SymToStringTag, valueProp(asciiString("Reflect"), false, false, true)) + + return o +} + +func (r *Runtime) getReflect() *Object { + ret := r.global.Reflect + if ret == nil { + ret = &Object{runtime: r} + r.global.Reflect = ret + ret.self = r.createReflect(ret) + } + return ret +} diff --git a/goja/builtin_regexp.go b/goja/builtin_regexp.go new file mode 100644 index 0000000..cdc0d9d --- /dev/null +++ b/goja/builtin_regexp.go @@ -0,0 +1,1348 @@ +package goja + +import ( + "fmt" + "github.com/dop251/goja/parser" + "regexp" + "strings" + "unicode/utf16" + "unicode/utf8" +) + +func (r *Runtime) newRegexpObject(proto *Object) *regexpObject { + v := &Object{runtime: r} + + o := ®expObject{} + o.class = classRegExp + o.val = v + o.extensible = true + v.self = o + o.prototype = proto + o.init() + return o +} + +func (r *Runtime) newRegExpp(pattern *regexpPattern, patternStr String, proto *Object) *regexpObject { + o := r.newRegexpObject(proto) + + o.pattern = pattern + o.source = patternStr + + return o +} + +func decodeHex(s string) (int, bool) { + var hex int + for i := 0; i < len(s); i++ { + var n byte + chr := s[i] + switch { + case '0' <= chr && chr <= '9': + n = chr - '0' + case 'a' <= chr && chr <= 'f': + n = chr - 'a' + 10 + case 'A' <= chr && chr <= 'F': + n = chr - 'A' + 10 + default: + return 0, false + } + hex = hex*16 + int(n) + } + return hex, true +} + +func writeHex4(b *strings.Builder, i int) { + b.WriteByte(hex[i>>12]) + b.WriteByte(hex[(i>>8)&0xF]) + b.WriteByte(hex[(i>>4)&0xF]) + b.WriteByte(hex[i&0xF]) +} + +// Convert any valid surrogate pairs in the form of \uXXXX\uXXXX to unicode characters +func convertRegexpToUnicode(patternStr string) string { + var sb strings.Builder + pos := 0 + for i := 0; i < len(patternStr)-11; { + r, size := utf8.DecodeRuneInString(patternStr[i:]) + if r == '\\' { + i++ + if patternStr[i] == 'u' && patternStr[i+5] == '\\' && patternStr[i+6] == 'u' { + if first, ok := decodeHex(patternStr[i+1 : i+5]); ok { + if isUTF16FirstSurrogate(uint16(first)) { + if second, ok := decodeHex(patternStr[i+7 : i+11]); ok { + if isUTF16SecondSurrogate(uint16(second)) { + r = utf16.DecodeRune(rune(first), rune(second)) + sb.WriteString(patternStr[pos : i-1]) + sb.WriteRune(r) + i += 11 + pos = i + continue + } + } + } + } + } + i++ + } else { + i += size + } + } + if pos > 0 { + sb.WriteString(patternStr[pos:]) + return sb.String() + } + return patternStr +} + +// Convert any extended unicode characters to UTF-16 in the form of \uXXXX\uXXXX +func convertRegexpToUtf16(patternStr string) string { + var sb strings.Builder + pos := 0 + var prevRune rune + for i := 0; i < len(patternStr); { + r, size := utf8.DecodeRuneInString(patternStr[i:]) + if r > 0xFFFF { + sb.WriteString(patternStr[pos:i]) + if prevRune == '\\' { + sb.WriteRune('\\') + } + first, second := utf16.EncodeRune(r) + sb.WriteString(`\u`) + writeHex4(&sb, int(first)) + sb.WriteString(`\u`) + writeHex4(&sb, int(second)) + pos = i + size + } + i += size + prevRune = r + } + if pos > 0 { + sb.WriteString(patternStr[pos:]) + return sb.String() + } + return patternStr +} + +// convert any broken UTF-16 surrogate pairs to \uXXXX +func escapeInvalidUtf16(s String) string { + if imported, ok := s.(*importedString); ok { + return imported.s + } + if ascii, ok := s.(asciiString); ok { + return ascii.String() + } + var sb strings.Builder + rd := &lenientUtf16Decoder{utf16Reader: s.utf16Reader()} + pos := 0 + utf8Size := 0 + var utf8Buf [utf8.UTFMax]byte + for { + c, size, err := rd.ReadRune() + if err != nil { + break + } + if utf16.IsSurrogate(c) { + if sb.Len() == 0 { + sb.Grow(utf8Size + 7) + hrd := s.Reader() + var c rune + for p := 0; p < pos; { + var size int + var err error + c, size, err = hrd.ReadRune() + if err != nil { + // will not happen + panic(fmt.Errorf("error while reading string head %q, pos: %d: %w", s.String(), pos, err)) + } + sb.WriteRune(c) + p += size + } + if c == '\\' { + sb.WriteRune(c) + } + } + sb.WriteString(`\u`) + writeHex4(&sb, int(c)) + } else { + if sb.Len() > 0 { + sb.WriteRune(c) + } else { + utf8Size += utf8.EncodeRune(utf8Buf[:], c) + pos += size + } + } + } + if sb.Len() > 0 { + return sb.String() + } + return s.String() +} + +func compileRegexpFromValueString(patternStr String, flags string) (*regexpPattern, error) { + return compileRegexp(escapeInvalidUtf16(patternStr), flags) +} + +func compileRegexp(patternStr, flags string) (p *regexpPattern, err error) { + var global, ignoreCase, multiline, dotAll, sticky, unicode bool + var wrapper *regexpWrapper + var wrapper2 *regexp2Wrapper + + if flags != "" { + invalidFlags := func() { + err = fmt.Errorf("Invalid flags supplied to RegExp constructor '%s'", flags) + } + for _, chr := range flags { + switch chr { + case 'g': + if global { + invalidFlags() + return + } + global = true + case 'm': + if multiline { + invalidFlags() + return + } + multiline = true + case 's': + if dotAll { + invalidFlags() + return + } + dotAll = true + case 'i': + if ignoreCase { + invalidFlags() + return + } + ignoreCase = true + case 'y': + if sticky { + invalidFlags() + return + } + sticky = true + case 'u': + if unicode { + invalidFlags() + } + unicode = true + default: + invalidFlags() + return + } + } + } + + if unicode { + patternStr = convertRegexpToUnicode(patternStr) + } else { + patternStr = convertRegexpToUtf16(patternStr) + } + + re2Str, err1 := parser.TransformRegExp(patternStr, dotAll, unicode) + if err1 == nil { + re2flags := "" + if multiline { + re2flags += "m" + } + if dotAll { + re2flags += "s" + } + if ignoreCase { + re2flags += "i" + } + if len(re2flags) > 0 { + re2Str = fmt.Sprintf("(?%s:%s)", re2flags, re2Str) + } + + pattern, err1 := regexp.Compile(re2Str) + if err1 != nil { + err = fmt.Errorf("Invalid regular expression (re2): %s (%v)", re2Str, err1) + return + } + wrapper = (*regexpWrapper)(pattern) + } else { + if _, incompat := err1.(parser.RegexpErrorIncompatible); !incompat { + err = err1 + return + } + wrapper2, err = compileRegexp2(patternStr, multiline, dotAll, ignoreCase, unicode) + if err != nil { + err = fmt.Errorf("Invalid regular expression (regexp2): %s (%v)", patternStr, err) + return + } + } + + p = ®expPattern{ + src: patternStr, + regexpWrapper: wrapper, + regexp2Wrapper: wrapper2, + global: global, + ignoreCase: ignoreCase, + multiline: multiline, + dotAll: dotAll, + sticky: sticky, + unicode: unicode, + } + return +} + +func (r *Runtime) _newRegExp(patternStr String, flags string, proto *Object) *regexpObject { + pattern, err := compileRegexpFromValueString(patternStr, flags) + if err != nil { + panic(r.newSyntaxError(err.Error(), -1)) + } + return r.newRegExpp(pattern, patternStr, proto) +} + +func (r *Runtime) builtin_newRegExp(args []Value, proto *Object) *Object { + var patternVal, flagsVal Value + if len(args) > 0 { + patternVal = args[0] + } + if len(args) > 1 { + flagsVal = args[1] + } + return r.newRegExp(patternVal, flagsVal, proto).val +} + +func (r *Runtime) newRegExp(patternVal, flagsVal Value, proto *Object) *regexpObject { + var pattern String + var flags string + if isRegexp(patternVal) { // this may have side effects so need to call it anyway + if obj, ok := patternVal.(*Object); ok { + if rx, ok := obj.self.(*regexpObject); ok { + if flagsVal == nil || flagsVal == _undefined { + return rx.clone() + } else { + return r._newRegExp(rx.source, flagsVal.toString().String(), proto) + } + } else { + pattern = nilSafe(obj.self.getStr("source", nil)).toString() + if flagsVal == nil || flagsVal == _undefined { + flags = nilSafe(obj.self.getStr("flags", nil)).toString().String() + } else { + flags = flagsVal.toString().String() + } + goto exit + } + } + } + + if patternVal != nil && patternVal != _undefined { + pattern = patternVal.toString() + } + if flagsVal != nil && flagsVal != _undefined { + flags = flagsVal.toString().String() + } + + if pattern == nil { + pattern = stringEmpty + } +exit: + return r._newRegExp(pattern, flags, proto) +} + +func (r *Runtime) builtin_RegExp(call FunctionCall) Value { + pattern := call.Argument(0) + patternIsRegExp := isRegexp(pattern) + flags := call.Argument(1) + if patternIsRegExp && flags == _undefined { + if obj, ok := call.Argument(0).(*Object); ok { + patternConstructor := obj.self.getStr("constructor", nil) + if patternConstructor == r.global.RegExp { + return pattern + } + } + } + return r.newRegExp(pattern, flags, r.getRegExpPrototype()).val +} + +func (r *Runtime) regexpproto_compile(call FunctionCall) Value { + if this, ok := r.toObject(call.This).self.(*regexpObject); ok { + var ( + pattern *regexpPattern + source String + flags string + err error + ) + patternVal := call.Argument(0) + flagsVal := call.Argument(1) + if o, ok := patternVal.(*Object); ok { + if p, ok := o.self.(*regexpObject); ok { + if flagsVal != _undefined { + panic(r.NewTypeError("Cannot supply flags when constructing one RegExp from another")) + } + this.pattern = p.pattern + this.source = p.source + goto exit + } + } + if patternVal != _undefined { + source = patternVal.toString() + } else { + source = stringEmpty + } + if flagsVal != _undefined { + flags = flagsVal.toString().String() + } + pattern, err = compileRegexpFromValueString(source, flags) + if err != nil { + panic(r.newSyntaxError(err.Error(), -1)) + } + this.pattern = pattern + this.source = source + exit: + this.setOwnStr("lastIndex", intToValue(0), true) + return call.This + } + + panic(r.NewTypeError("Method RegExp.prototype.compile called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) regexpproto_exec(call FunctionCall) Value { + if this, ok := r.toObject(call.This).self.(*regexpObject); ok { + return this.exec(call.Argument(0).toString()) + } else { + r.typeErrorResult(true, "Method RegExp.prototype.exec called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This})) + return nil + } +} + +func (r *Runtime) regexpproto_test(call FunctionCall) Value { + if this, ok := r.toObject(call.This).self.(*regexpObject); ok { + if this.test(call.Argument(0).toString()) { + return valueTrue + } else { + return valueFalse + } + } else { + panic(r.NewTypeError("Method RegExp.prototype.test called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) + } +} + +func (r *Runtime) regexpproto_toString(call FunctionCall) Value { + obj := r.toObject(call.This) + if this := r.checkStdRegexp(obj); this != nil { + var sb StringBuilder + sb.WriteRune('/') + if !this.writeEscapedSource(&sb) { + sb.WriteString(this.source) + } + sb.WriteRune('/') + if this.pattern.global { + sb.WriteRune('g') + } + if this.pattern.ignoreCase { + sb.WriteRune('i') + } + if this.pattern.multiline { + sb.WriteRune('m') + } + if this.pattern.dotAll { + sb.WriteRune('s') + } + if this.pattern.unicode { + sb.WriteRune('u') + } + if this.pattern.sticky { + sb.WriteRune('y') + } + return sb.String() + } + pattern := nilSafe(obj.self.getStr("source", nil)).toString() + flags := nilSafe(obj.self.getStr("flags", nil)).toString() + var sb StringBuilder + sb.WriteRune('/') + sb.WriteString(pattern) + sb.WriteRune('/') + sb.WriteString(flags) + return sb.String() +} + +func (r *regexpObject) writeEscapedSource(sb *StringBuilder) bool { + if r.source.Length() == 0 { + sb.WriteString(asciiString("(?:)")) + return true + } + pos := 0 + lastPos := 0 + rd := &lenientUtf16Decoder{utf16Reader: r.source.utf16Reader()} +L: + for { + c, size, err := rd.ReadRune() + if err != nil { + break + } + switch c { + case '\\': + pos++ + _, size, err = rd.ReadRune() + if err != nil { + break L + } + case '/', '\u000a', '\u000d', '\u2028', '\u2029': + sb.WriteSubstring(r.source, lastPos, pos) + sb.WriteRune('\\') + switch c { + case '\u000a': + sb.WriteRune('n') + case '\u000d': + sb.WriteRune('r') + default: + sb.WriteRune('u') + sb.WriteRune(rune(hex[c>>12])) + sb.WriteRune(rune(hex[(c>>8)&0xF])) + sb.WriteRune(rune(hex[(c>>4)&0xF])) + sb.WriteRune(rune(hex[c&0xF])) + } + lastPos = pos + size + } + pos += size + } + if lastPos > 0 { + sb.WriteSubstring(r.source, lastPos, r.source.Length()) + return true + } + return false +} + +func (r *Runtime) regexpproto_getSource(call FunctionCall) Value { + if this, ok := r.toObject(call.This).self.(*regexpObject); ok { + var sb StringBuilder + if this.writeEscapedSource(&sb) { + return sb.String() + } + return this.source + } else if call.This == r.global.RegExpPrototype { + return asciiString("(?:)") + } else { + panic(r.NewTypeError("Method RegExp.prototype.source getter called on incompatible receiver")) + } +} + +func (r *Runtime) regexpproto_getGlobal(call FunctionCall) Value { + if this, ok := r.toObject(call.This).self.(*regexpObject); ok { + if this.pattern.global { + return valueTrue + } else { + return valueFalse + } + } else if call.This == r.global.RegExpPrototype { + return _undefined + } else { + panic(r.NewTypeError("Method RegExp.prototype.global getter called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) + } +} + +func (r *Runtime) regexpproto_getMultiline(call FunctionCall) Value { + if this, ok := r.toObject(call.This).self.(*regexpObject); ok { + if this.pattern.multiline { + return valueTrue + } else { + return valueFalse + } + } else if call.This == r.global.RegExpPrototype { + return _undefined + } else { + panic(r.NewTypeError("Method RegExp.prototype.multiline getter called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) + } +} + +func (r *Runtime) regexpproto_getDotAll(call FunctionCall) Value { + if this, ok := r.toObject(call.This).self.(*regexpObject); ok { + if this.pattern.dotAll { + return valueTrue + } else { + return valueFalse + } + } else if call.This == r.global.RegExpPrototype { + return _undefined + } else { + panic(r.NewTypeError("Method RegExp.prototype.dotAll getter called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) + } +} + +func (r *Runtime) regexpproto_getIgnoreCase(call FunctionCall) Value { + if this, ok := r.toObject(call.This).self.(*regexpObject); ok { + if this.pattern.ignoreCase { + return valueTrue + } else { + return valueFalse + } + } else if call.This == r.global.RegExpPrototype { + return _undefined + } else { + panic(r.NewTypeError("Method RegExp.prototype.ignoreCase getter called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) + } +} + +func (r *Runtime) regexpproto_getUnicode(call FunctionCall) Value { + if this, ok := r.toObject(call.This).self.(*regexpObject); ok { + if this.pattern.unicode { + return valueTrue + } else { + return valueFalse + } + } else if call.This == r.global.RegExpPrototype { + return _undefined + } else { + panic(r.NewTypeError("Method RegExp.prototype.unicode getter called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) + } +} + +func (r *Runtime) regexpproto_getSticky(call FunctionCall) Value { + if this, ok := r.toObject(call.This).self.(*regexpObject); ok { + if this.pattern.sticky { + return valueTrue + } else { + return valueFalse + } + } else if call.This == r.global.RegExpPrototype { + return _undefined + } else { + panic(r.NewTypeError("Method RegExp.prototype.sticky getter called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) + } +} + +func (r *Runtime) regexpproto_getFlags(call FunctionCall) Value { + var global, ignoreCase, multiline, dotAll, sticky, unicode bool + + thisObj := r.toObject(call.This) + size := 0 + if v := thisObj.self.getStr("global", nil); v != nil { + global = v.ToBoolean() + if global { + size++ + } + } + if v := thisObj.self.getStr("ignoreCase", nil); v != nil { + ignoreCase = v.ToBoolean() + if ignoreCase { + size++ + } + } + if v := thisObj.self.getStr("multiline", nil); v != nil { + multiline = v.ToBoolean() + if multiline { + size++ + } + } + if v := thisObj.self.getStr("dotAll", nil); v != nil { + dotAll = v.ToBoolean() + if dotAll { + size++ + } + } + if v := thisObj.self.getStr("sticky", nil); v != nil { + sticky = v.ToBoolean() + if sticky { + size++ + } + } + if v := thisObj.self.getStr("unicode", nil); v != nil { + unicode = v.ToBoolean() + if unicode { + size++ + } + } + + var sb strings.Builder + sb.Grow(size) + if global { + sb.WriteByte('g') + } + if ignoreCase { + sb.WriteByte('i') + } + if multiline { + sb.WriteByte('m') + } + if dotAll { + sb.WriteByte('s') + } + if unicode { + sb.WriteByte('u') + } + if sticky { + sb.WriteByte('y') + } + + return asciiString(sb.String()) +} + +func (r *Runtime) regExpExec(execFn func(FunctionCall) Value, rxObj *Object, arg Value) Value { + res := execFn(FunctionCall{ + This: rxObj, + Arguments: []Value{arg}, + }) + + if res != _null { + if _, ok := res.(*Object); !ok { + panic(r.NewTypeError("RegExp exec method returned something other than an Object or null")) + } + } + + return res +} + +func (r *Runtime) getGlobalRegexpMatches(rxObj *Object, s String, fullUnicode bool) []Value { + rxObj.self.setOwnStr("lastIndex", intToValue(0), true) + execFn, ok := r.toObject(rxObj.self.getStr("exec", nil)).self.assertCallable() + if !ok { + panic(r.NewTypeError("exec is not a function")) + } + var a []Value + for { + res := r.regExpExec(execFn, rxObj, s) + if res == _null { + break + } + a = append(a, res) + matchStr := nilSafe(r.toObject(res).self.getIdx(valueInt(0), nil)).toString() + if matchStr.Length() == 0 { + thisIndex := toLength(rxObj.self.getStr("lastIndex", nil)) + rxObj.self.setOwnStr("lastIndex", valueInt(advanceStringIndex64(s, thisIndex, fullUnicode)), true) + } + } + + return a +} + +func (r *Runtime) regexpproto_stdMatcherGeneric(rxObj *Object, s String) Value { + rx := rxObj.self + flags := nilSafe(rx.getStr("flags", nil)).String() + global := strings.ContainsRune(flags, 'g') + if global { + a := r.getGlobalRegexpMatches(rxObj, s, strings.ContainsRune(flags, 'u')) + if len(a) == 0 { + return _null + } + ar := make([]Value, 0, len(a)) + for _, result := range a { + obj := r.toObject(result) + matchStr := nilSafe(obj.self.getIdx(valueInt(0), nil)).ToString() + ar = append(ar, matchStr) + } + return r.newArrayValues(ar) + } + + execFn, ok := r.toObject(rx.getStr("exec", nil)).self.assertCallable() + if !ok { + panic(r.NewTypeError("exec is not a function")) + } + + return r.regExpExec(execFn, rxObj, s) +} + +func (r *Runtime) checkStdRegexp(rxObj *Object) *regexpObject { + if deoptimiseRegexp { + return nil + } + + rx, ok := rxObj.self.(*regexpObject) + if !ok { + return nil + } + + if !rx.standard || rx.prototype == nil || rx.prototype.self != r.global.stdRegexpProto { + return nil + } + + return rx +} + +func (r *Runtime) regexpproto_stdMatcher(call FunctionCall) Value { + thisObj := r.toObject(call.This) + s := call.Argument(0).toString() + rx := r.checkStdRegexp(thisObj) + if rx == nil { + return r.regexpproto_stdMatcherGeneric(thisObj, s) + } + if rx.pattern.global { + res := rx.pattern.findAllSubmatchIndex(s, 0, -1, rx.pattern.sticky) + if len(res) == 0 { + rx.setOwnStr("lastIndex", intToValue(0), true) + return _null + } + a := make([]Value, 0, len(res)) + for _, result := range res { + a = append(a, s.Substring(result[0], result[1])) + } + rx.setOwnStr("lastIndex", intToValue(int64(res[len(res)-1][1])), true) + return r.newArrayValues(a) + } else { + return rx.exec(s) + } +} + +func (r *Runtime) regexpproto_stdSearchGeneric(rxObj *Object, arg String) Value { + rx := rxObj.self + previousLastIndex := nilSafe(rx.getStr("lastIndex", nil)) + zero := intToValue(0) + if !previousLastIndex.SameAs(zero) { + rx.setOwnStr("lastIndex", zero, true) + } + execFn, ok := r.toObject(rx.getStr("exec", nil)).self.assertCallable() + if !ok { + panic(r.NewTypeError("exec is not a function")) + } + + result := r.regExpExec(execFn, rxObj, arg) + currentLastIndex := nilSafe(rx.getStr("lastIndex", nil)) + if !currentLastIndex.SameAs(previousLastIndex) { + rx.setOwnStr("lastIndex", previousLastIndex, true) + } + + if result == _null { + return intToValue(-1) + } + + return r.toObject(result).self.getStr("index", nil) +} + +func (r *Runtime) regexpproto_stdMatcherAll(call FunctionCall) Value { + thisObj := r.toObject(call.This) + s := call.Argument(0).toString() + flags := nilSafe(thisObj.self.getStr("flags", nil)).toString() + c := r.speciesConstructorObj(call.This.(*Object), r.getRegExp()) + matcher := r.toConstructor(c)([]Value{call.This, flags}, nil) + matcher.self.setOwnStr("lastIndex", valueInt(toLength(thisObj.self.getStr("lastIndex", nil))), true) + flagsStr := flags.String() + global := strings.Contains(flagsStr, "g") + fullUnicode := strings.Contains(flagsStr, "u") + return r.createRegExpStringIterator(matcher, s, global, fullUnicode) +} + +func (r *Runtime) createRegExpStringIterator(matcher *Object, s String, global, fullUnicode bool) Value { + o := &Object{runtime: r} + + ri := ®ExpStringIterObject{ + matcher: matcher, + s: s, + global: global, + fullUnicode: fullUnicode, + } + ri.class = classObject + ri.val = o + ri.extensible = true + o.self = ri + ri.prototype = r.getRegExpStringIteratorPrototype() + ri.init() + + return o +} + +type regExpStringIterObject struct { + baseObject + matcher *Object + s String + global, fullUnicode, done bool +} + +// RegExpExec as defined in 21.2.5.2.1 +func regExpExec(r *Object, s String) Value { + exec := r.self.getStr("exec", nil) + if execObject, ok := exec.(*Object); ok { + if execFn, ok := execObject.self.assertCallable(); ok { + return r.runtime.regExpExec(execFn, r, s) + } + } + if rx, ok := r.self.(*regexpObject); ok { + return rx.exec(s) + } + panic(r.runtime.NewTypeError("no RegExpMatcher internal slot")) +} + +func (ri *regExpStringIterObject) next() (v Value) { + if ri.done { + return ri.val.runtime.createIterResultObject(_undefined, true) + } + + match := regExpExec(ri.matcher, ri.s) + if IsNull(match) { + ri.done = true + return ri.val.runtime.createIterResultObject(_undefined, true) + } + if !ri.global { + ri.done = true + return ri.val.runtime.createIterResultObject(match, false) + } + + matchStr := nilSafe(ri.val.runtime.toObject(match).self.getIdx(valueInt(0), nil)).toString() + if matchStr.Length() == 0 { + thisIndex := toLength(ri.matcher.self.getStr("lastIndex", nil)) + ri.matcher.self.setOwnStr("lastIndex", valueInt(advanceStringIndex64(ri.s, thisIndex, ri.fullUnicode)), true) + } + return ri.val.runtime.createIterResultObject(match, false) +} + +func (r *Runtime) regexpproto_stdSearch(call FunctionCall) Value { + thisObj := r.toObject(call.This) + s := call.Argument(0).toString() + rx := r.checkStdRegexp(thisObj) + if rx == nil { + return r.regexpproto_stdSearchGeneric(thisObj, s) + } + + previousLastIndex := rx.getStr("lastIndex", nil) + rx.setOwnStr("lastIndex", intToValue(0), true) + + match, result := rx.execRegexp(s) + rx.setOwnStr("lastIndex", previousLastIndex, true) + + if !match { + return intToValue(-1) + } + return intToValue(int64(result[0])) +} + +func (r *Runtime) regexpproto_stdSplitterGeneric(splitter *Object, s String, limit Value, unicodeMatching bool) Value { + var a []Value + var lim int64 + if limit == nil || limit == _undefined { + lim = maxInt - 1 + } else { + lim = toLength(limit) + } + if lim == 0 { + return r.newArrayValues(a) + } + size := s.Length() + p := 0 + execFn := toMethod(splitter.ToObject(r).self.getStr("exec", nil)) // must be non-nil + + if size == 0 { + if r.regExpExec(execFn, splitter, s) == _null { + a = append(a, s) + } + return r.newArrayValues(a) + } + + q := p + for q < size { + splitter.self.setOwnStr("lastIndex", intToValue(int64(q)), true) + z := r.regExpExec(execFn, splitter, s) + if z == _null { + q = advanceStringIndex(s, q, unicodeMatching) + } else { + z := r.toObject(z) + e := toLength(splitter.self.getStr("lastIndex", nil)) + if e == int64(p) { + q = advanceStringIndex(s, q, unicodeMatching) + } else { + a = append(a, s.Substring(p, q)) + if int64(len(a)) == lim { + return r.newArrayValues(a) + } + if e > int64(size) { + p = size + } else { + p = int(e) + } + numberOfCaptures := max(toLength(z.self.getStr("length", nil))-1, 0) + for i := int64(1); i <= numberOfCaptures; i++ { + a = append(a, nilSafe(z.self.getIdx(valueInt(i), nil))) + if int64(len(a)) == lim { + return r.newArrayValues(a) + } + } + q = p + } + } + } + a = append(a, s.Substring(p, size)) + return r.newArrayValues(a) +} + +func advanceStringIndex(s String, pos int, unicode bool) int { + next := pos + 1 + if !unicode { + return next + } + l := s.Length() + if next >= l { + return next + } + if !isUTF16FirstSurrogate(s.CharAt(pos)) { + return next + } + if !isUTF16SecondSurrogate(s.CharAt(next)) { + return next + } + return next + 1 +} + +func advanceStringIndex64(s String, pos int64, unicode bool) int64 { + next := pos + 1 + if !unicode { + return next + } + l := int64(s.Length()) + if next >= l { + return next + } + if !isUTF16FirstSurrogate(s.CharAt(int(pos))) { + return next + } + if !isUTF16SecondSurrogate(s.CharAt(int(next))) { + return next + } + return next + 1 +} + +func (r *Runtime) regexpproto_stdSplitter(call FunctionCall) Value { + rxObj := r.toObject(call.This) + s := call.Argument(0).toString() + limitValue := call.Argument(1) + var splitter *Object + search := r.checkStdRegexp(rxObj) + c := r.speciesConstructorObj(rxObj, r.getRegExp()) + if search == nil || c != r.global.RegExp { + flags := nilSafe(rxObj.self.getStr("flags", nil)).toString() + flagsStr := flags.String() + + // Add 'y' flag if missing + if !strings.Contains(flagsStr, "y") { + flags = flags.Concat(asciiString("y")) + } + splitter = r.toConstructor(c)([]Value{rxObj, flags}, nil) + search = r.checkStdRegexp(splitter) + if search == nil { + return r.regexpproto_stdSplitterGeneric(splitter, s, limitValue, strings.Contains(flagsStr, "u")) + } + } + + pattern := search.pattern // toUint32() may recompile the pattern, but we still need to use the original + limit := -1 + if limitValue != _undefined { + limit = int(toUint32(limitValue)) + } + + if limit == 0 { + return r.newArrayValues(nil) + } + + targetLength := s.Length() + var valueArray []Value + lastIndex := 0 + found := 0 + + result := pattern.findAllSubmatchIndex(s, 0, -1, false) + if targetLength == 0 { + if result == nil { + valueArray = append(valueArray, s) + } + goto RETURN + } + + for _, match := range result { + if match[0] == match[1] { + // FIXME Ugh, this is a hack + if match[0] == 0 || match[0] == targetLength { + continue + } + } + + if lastIndex != match[0] { + valueArray = append(valueArray, s.Substring(lastIndex, match[0])) + found++ + } else if lastIndex == match[0] { + if lastIndex != -1 { + valueArray = append(valueArray, stringEmpty) + found++ + } + } + + lastIndex = match[1] + if found == limit { + goto RETURN + } + + captureCount := len(match) / 2 + for index := 1; index < captureCount; index++ { + offset := index * 2 + var value Value + if match[offset] != -1 { + value = s.Substring(match[offset], match[offset+1]) + } else { + value = _undefined + } + valueArray = append(valueArray, value) + found++ + if found == limit { + goto RETURN + } + } + } + + if found != limit { + if lastIndex != targetLength { + valueArray = append(valueArray, s.Substring(lastIndex, targetLength)) + } else { + valueArray = append(valueArray, stringEmpty) + } + } + +RETURN: + return r.newArrayValues(valueArray) +} + +func (r *Runtime) regexpproto_stdReplacerGeneric(rxObj *Object, s, replaceStr String, rcall func(FunctionCall) Value) Value { + var results []Value + flags := nilSafe(rxObj.self.getStr("flags", nil)).String() + isGlobal := strings.ContainsRune(flags, 'g') + isUnicode := strings.ContainsRune(flags, 'u') + if isGlobal { + results = r.getGlobalRegexpMatches(rxObj, s, isUnicode) + } else { + execFn := toMethod(rxObj.self.getStr("exec", nil)) // must be non-nil + result := r.regExpExec(execFn, rxObj, s) + if result != _null { + results = append(results, result) + } + } + lengthS := s.Length() + nextSourcePosition := 0 + var resultBuf StringBuilder + for _, result := range results { + obj := r.toObject(result) + nCaptures := max(toLength(obj.self.getStr("length", nil))-1, 0) + matched := nilSafe(obj.self.getIdx(valueInt(0), nil)).toString() + matchLength := matched.Length() + position := toIntStrict(max(min(nilSafe(obj.self.getStr("index", nil)).ToInteger(), int64(lengthS)), 0)) + var captures []Value + if rcall != nil { + captures = make([]Value, 0, nCaptures+3) + } else { + captures = make([]Value, 0, nCaptures+1) + } + captures = append(captures, matched) + for n := int64(1); n <= nCaptures; n++ { + capN := nilSafe(obj.self.getIdx(valueInt(n), nil)) + if capN != _undefined { + capN = capN.ToString() + } + captures = append(captures, capN) + } + var replacement String + if rcall != nil { + captures = append(captures, intToValue(int64(position)), s) + replacement = rcall(FunctionCall{ + This: _undefined, + Arguments: captures, + }).toString() + if position >= nextSourcePosition { + resultBuf.WriteString(s.Substring(nextSourcePosition, position)) + resultBuf.WriteString(replacement) + nextSourcePosition = position + matchLength + } + } else { + if position >= nextSourcePosition { + resultBuf.WriteString(s.Substring(nextSourcePosition, position)) + writeSubstitution(s, position, len(captures), func(idx int) String { + capture := captures[idx] + if capture != _undefined { + return capture.toString() + } + return stringEmpty + }, replaceStr, &resultBuf) + nextSourcePosition = position + matchLength + } + } + } + if nextSourcePosition < lengthS { + resultBuf.WriteString(s.Substring(nextSourcePosition, lengthS)) + } + return resultBuf.String() +} + +func writeSubstitution(s String, position int, numCaptures int, getCapture func(int) String, replaceStr String, buf *StringBuilder) { + l := s.Length() + rl := replaceStr.Length() + matched := getCapture(0) + tailPos := position + matched.Length() + + for i := 0; i < rl; i++ { + c := replaceStr.CharAt(i) + if c == '$' && i < rl-1 { + ch := replaceStr.CharAt(i + 1) + switch ch { + case '$': + buf.WriteRune('$') + case '`': + buf.WriteString(s.Substring(0, position)) + case '\'': + if tailPos < l { + buf.WriteString(s.Substring(tailPos, l)) + } + case '&': + buf.WriteString(matched) + default: + matchNumber := 0 + j := i + 1 + for j < rl { + ch := replaceStr.CharAt(j) + if ch >= '0' && ch <= '9' { + m := matchNumber*10 + int(ch-'0') + if m >= numCaptures { + break + } + matchNumber = m + j++ + } else { + break + } + } + if matchNumber > 0 { + buf.WriteString(getCapture(matchNumber)) + i = j - 1 + continue + } else { + buf.WriteRune('$') + buf.WriteRune(rune(ch)) + } + } + i++ + } else { + buf.WriteRune(rune(c)) + } + } +} + +func (r *Runtime) regexpproto_stdReplacer(call FunctionCall) Value { + rxObj := r.toObject(call.This) + s := call.Argument(0).toString() + replaceStr, rcall := getReplaceValue(call.Argument(1)) + + rx := r.checkStdRegexp(rxObj) + if rx == nil { + return r.regexpproto_stdReplacerGeneric(rxObj, s, replaceStr, rcall) + } + + var index int64 + find := 1 + if rx.pattern.global { + find = -1 + rx.setOwnStr("lastIndex", intToValue(0), true) + } else { + index = rx.getLastIndex() + } + found := rx.pattern.findAllSubmatchIndex(s, toIntStrict(index), find, rx.pattern.sticky) + if len(found) > 0 { + if !rx.updateLastIndex(index, found[0], found[len(found)-1]) { + found = nil + } + } else { + rx.updateLastIndex(index, nil, nil) + } + + return stringReplace(s, found, replaceStr, rcall) +} + +func (r *Runtime) regExpStringIteratorProto_next(call FunctionCall) Value { + thisObj := r.toObject(call.This) + if iter, ok := thisObj.self.(*regExpStringIterObject); ok { + return iter.next() + } + panic(r.NewTypeError("Method RegExp String Iterator.prototype.next called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) +} + +func (r *Runtime) createRegExpStringIteratorPrototype(val *Object) objectImpl { + o := newBaseObjectObj(val, r.getIteratorPrototype(), classObject) + + o._putProp("next", r.newNativeFunc(r.regExpStringIteratorProto_next, "next", 0), true, false, true) + o._putSym(SymToStringTag, valueProp(asciiString(classRegExpStringIterator), false, false, true)) + + return o +} + +func (r *Runtime) getRegExpStringIteratorPrototype() *Object { + var o *Object + if o = r.global.RegExpStringIteratorPrototype; o == nil { + o = &Object{runtime: r} + r.global.RegExpStringIteratorPrototype = o + o.self = r.createRegExpStringIteratorPrototype(o) + } + return o +} + +func (r *Runtime) getRegExp() *Object { + ret := r.global.RegExp + if ret == nil { + ret = &Object{runtime: r} + r.global.RegExp = ret + proto := r.getRegExpPrototype() + r.newNativeFuncAndConstruct(ret, r.builtin_RegExp, + r.wrapNativeConstruct(r.builtin_newRegExp, ret, proto), proto, "RegExp", intToValue(2)) + rx := ret.self + r.putSpeciesReturnThis(rx) + } + return ret +} + +func (r *Runtime) getRegExpPrototype() *Object { + ret := r.global.RegExpPrototype + if ret == nil { + o := r.newGuardedObject(r.global.ObjectPrototype, classObject) + ret = o.val + r.global.RegExpPrototype = ret + r.global.stdRegexpProto = o + + o._putProp("constructor", r.getRegExp(), true, false, true) + o._putProp("compile", r.newNativeFunc(r.regexpproto_compile, "compile", 2), true, false, true) + o._putProp("exec", r.newNativeFunc(r.regexpproto_exec, "exec", 1), true, false, true) + o._putProp("test", r.newNativeFunc(r.regexpproto_test, "test", 1), true, false, true) + o._putProp("toString", r.newNativeFunc(r.regexpproto_toString, "toString", 0), true, false, true) + o.setOwnStr("source", &valueProperty{ + configurable: true, + getterFunc: r.newNativeFunc(r.regexpproto_getSource, "get source", 0), + accessor: true, + }, false) + o.setOwnStr("global", &valueProperty{ + configurable: true, + getterFunc: r.newNativeFunc(r.regexpproto_getGlobal, "get global", 0), + accessor: true, + }, false) + o.setOwnStr("multiline", &valueProperty{ + configurable: true, + getterFunc: r.newNativeFunc(r.regexpproto_getMultiline, "get multiline", 0), + accessor: true, + }, false) + o.setOwnStr("dotAll", &valueProperty{ + configurable: true, + getterFunc: r.newNativeFunc(r.regexpproto_getDotAll, "get dotAll", 0), + accessor: true, + }, false) + o.setOwnStr("ignoreCase", &valueProperty{ + configurable: true, + getterFunc: r.newNativeFunc(r.regexpproto_getIgnoreCase, "get ignoreCase", 0), + accessor: true, + }, false) + o.setOwnStr("unicode", &valueProperty{ + configurable: true, + getterFunc: r.newNativeFunc(r.regexpproto_getUnicode, "get unicode", 0), + accessor: true, + }, false) + o.setOwnStr("sticky", &valueProperty{ + configurable: true, + getterFunc: r.newNativeFunc(r.regexpproto_getSticky, "get sticky", 0), + accessor: true, + }, false) + o.setOwnStr("flags", &valueProperty{ + configurable: true, + getterFunc: r.newNativeFunc(r.regexpproto_getFlags, "get flags", 0), + accessor: true, + }, false) + + o._putSym(SymMatch, valueProp(r.newNativeFunc(r.regexpproto_stdMatcher, "[Symbol.match]", 1), true, false, true)) + o._putSym(SymMatchAll, valueProp(r.newNativeFunc(r.regexpproto_stdMatcherAll, "[Symbol.matchAll]", 1), true, false, true)) + o._putSym(SymSearch, valueProp(r.newNativeFunc(r.regexpproto_stdSearch, "[Symbol.search]", 1), true, false, true)) + o._putSym(SymSplit, valueProp(r.newNativeFunc(r.regexpproto_stdSplitter, "[Symbol.split]", 2), true, false, true)) + o._putSym(SymReplace, valueProp(r.newNativeFunc(r.regexpproto_stdReplacer, "[Symbol.replace]", 2), true, false, true)) + o.guard("exec", "global", "multiline", "ignoreCase", "unicode", "sticky") + } + return ret +} diff --git a/goja/builtin_set.go b/goja/builtin_set.go new file mode 100644 index 0000000..eeedb88 --- /dev/null +++ b/goja/builtin_set.go @@ -0,0 +1,346 @@ +package goja + +import ( + "fmt" + "reflect" +) + +var setExportType = reflectTypeArray + +type setObject struct { + baseObject + m *orderedMap +} + +type setIterObject struct { + baseObject + iter *orderedMapIter + kind iterationKind +} + +func (o *setIterObject) next() Value { + if o.iter == nil { + return o.val.runtime.createIterResultObject(_undefined, true) + } + + entry := o.iter.next() + if entry == nil { + o.iter = nil + return o.val.runtime.createIterResultObject(_undefined, true) + } + + var result Value + switch o.kind { + case iterationKindValue: + result = entry.key + default: + result = o.val.runtime.newArrayValues([]Value{entry.key, entry.key}) + } + + return o.val.runtime.createIterResultObject(result, false) +} + +func (so *setObject) init() { + so.baseObject.init() + so.m = newOrderedMap(so.val.runtime.getHash()) +} + +func (so *setObject) exportType() reflect.Type { + return setExportType +} + +func (so *setObject) export(ctx *objectExportCtx) interface{} { + a := make([]interface{}, so.m.size) + ctx.put(so.val, a) + iter := so.m.newIter() + for i := 0; i < len(a); i++ { + entry := iter.next() + if entry == nil { + break + } + a[i] = exportValue(entry.key, ctx) + } + return a +} + +func (so *setObject) exportToArrayOrSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + l := so.m.size + if typ.Kind() == reflect.Array { + if dst.Len() != l { + return fmt.Errorf("cannot convert a Set into an array, lengths mismatch: have %d, need %d)", l, dst.Len()) + } + } else { + dst.Set(reflect.MakeSlice(typ, l, l)) + } + ctx.putTyped(so.val, typ, dst.Interface()) + iter := so.m.newIter() + r := so.val.runtime + for i := 0; i < l; i++ { + entry := iter.next() + if entry == nil { + break + } + err := r.toReflectValue(entry.key, dst.Index(i), ctx) + if err != nil { + return err + } + } + return nil +} + +func (so *setObject) exportToMap(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + dst.Set(reflect.MakeMap(typ)) + keyTyp := typ.Key() + elemTyp := typ.Elem() + iter := so.m.newIter() + r := so.val.runtime + for { + entry := iter.next() + if entry == nil { + break + } + keyVal := reflect.New(keyTyp).Elem() + err := r.toReflectValue(entry.key, keyVal, ctx) + if err != nil { + return err + } + dst.SetMapIndex(keyVal, reflect.Zero(elemTyp)) + } + return nil +} + +func (r *Runtime) setProto_add(call FunctionCall) Value { + thisObj := r.toObject(call.This) + so, ok := thisObj.self.(*setObject) + if !ok { + panic(r.NewTypeError("Method Set.prototype.add called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + + so.m.set(call.Argument(0), nil) + return call.This +} + +func (r *Runtime) setProto_clear(call FunctionCall) Value { + thisObj := r.toObject(call.This) + so, ok := thisObj.self.(*setObject) + if !ok { + panic(r.NewTypeError("Method Set.prototype.clear called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + + so.m.clear() + return _undefined +} + +func (r *Runtime) setProto_delete(call FunctionCall) Value { + thisObj := r.toObject(call.This) + so, ok := thisObj.self.(*setObject) + if !ok { + panic(r.NewTypeError("Method Set.prototype.delete called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + + return r.toBoolean(so.m.remove(call.Argument(0))) +} + +func (r *Runtime) setProto_entries(call FunctionCall) Value { + return r.createSetIterator(call.This, iterationKindKeyValue) +} + +func (r *Runtime) setProto_forEach(call FunctionCall) Value { + thisObj := r.toObject(call.This) + so, ok := thisObj.self.(*setObject) + if !ok { + panic(r.NewTypeError("Method Set.prototype.forEach called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + callbackFn, ok := r.toObject(call.Argument(0)).self.assertCallable() + if !ok { + panic(r.NewTypeError("object is not a function %s")) + } + t := call.Argument(1) + iter := so.m.newIter() + for { + entry := iter.next() + if entry == nil { + break + } + callbackFn(FunctionCall{This: t, Arguments: []Value{entry.key, entry.key, thisObj}}) + } + + return _undefined +} + +func (r *Runtime) setProto_has(call FunctionCall) Value { + thisObj := r.toObject(call.This) + so, ok := thisObj.self.(*setObject) + if !ok { + panic(r.NewTypeError("Method Set.prototype.has called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + + return r.toBoolean(so.m.has(call.Argument(0))) +} + +func (r *Runtime) setProto_getSize(call FunctionCall) Value { + thisObj := r.toObject(call.This) + so, ok := thisObj.self.(*setObject) + if !ok { + panic(r.NewTypeError("Method get Set.prototype.size called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + + return intToValue(int64(so.m.size)) +} + +func (r *Runtime) setProto_values(call FunctionCall) Value { + return r.createSetIterator(call.This, iterationKindValue) +} + +func (r *Runtime) builtin_newSet(args []Value, newTarget *Object) *Object { + if newTarget == nil { + panic(r.needNew("Set")) + } + proto := r.getPrototypeFromCtor(newTarget, r.global.Set, r.global.SetPrototype) + o := &Object{runtime: r} + + so := &setObject{} + so.class = classObject + so.val = o + so.extensible = true + o.self = so + so.prototype = proto + so.init() + if len(args) > 0 { + if arg := args[0]; arg != nil && arg != _undefined && arg != _null { + adder := so.getStr("add", nil) + stdArr := r.checkStdArrayIter(arg) + if adder == r.global.setAdder { + if stdArr != nil { + for _, v := range stdArr.values { + so.m.set(v, nil) + } + } else { + r.getIterator(arg, nil).iterate(func(item Value) { + so.m.set(item, nil) + }) + } + } else { + adderFn := toMethod(adder) + if adderFn == nil { + panic(r.NewTypeError("Set.add in missing")) + } + if stdArr != nil { + for _, item := range stdArr.values { + adderFn(FunctionCall{This: o, Arguments: []Value{item}}) + } + } else { + r.getIterator(arg, nil).iterate(func(item Value) { + adderFn(FunctionCall{This: o, Arguments: []Value{item}}) + }) + } + } + } + } + return o +} + +func (r *Runtime) createSetIterator(setValue Value, kind iterationKind) Value { + obj := r.toObject(setValue) + setObj, ok := obj.self.(*setObject) + if !ok { + panic(r.NewTypeError("Object is not a Set")) + } + + o := &Object{runtime: r} + + si := &setIterObject{ + iter: setObj.m.newIter(), + kind: kind, + } + si.class = classObject + si.val = o + si.extensible = true + o.self = si + si.prototype = r.getSetIteratorPrototype() + si.init() + + return o +} + +func (r *Runtime) setIterProto_next(call FunctionCall) Value { + thisObj := r.toObject(call.This) + if iter, ok := thisObj.self.(*setIterObject); ok { + return iter.next() + } + panic(r.NewTypeError("Method Set Iterator.prototype.next called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) +} + +func (r *Runtime) createSetProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.global.ObjectPrototype, classObject) + + o._putProp("constructor", r.getSet(), true, false, true) + r.global.setAdder = r.newNativeFunc(r.setProto_add, "add", 1) + o._putProp("add", r.global.setAdder, true, false, true) + + o._putProp("clear", r.newNativeFunc(r.setProto_clear, "clear", 0), true, false, true) + o._putProp("delete", r.newNativeFunc(r.setProto_delete, "delete", 1), true, false, true) + o._putProp("forEach", r.newNativeFunc(r.setProto_forEach, "forEach", 1), true, false, true) + o._putProp("has", r.newNativeFunc(r.setProto_has, "has", 1), true, false, true) + o.setOwnStr("size", &valueProperty{ + getterFunc: r.newNativeFunc(r.setProto_getSize, "get size", 0), + accessor: true, + writable: true, + configurable: true, + }, true) + + valuesFunc := r.newNativeFunc(r.setProto_values, "values", 0) + o._putProp("values", valuesFunc, true, false, true) + o._putProp("keys", valuesFunc, true, false, true) + o._putProp("entries", r.newNativeFunc(r.setProto_entries, "entries", 0), true, false, true) + o._putSym(SymIterator, valueProp(valuesFunc, true, false, true)) + o._putSym(SymToStringTag, valueProp(asciiString(classSet), false, false, true)) + + return o +} + +func (r *Runtime) createSet(val *Object) objectImpl { + o := r.newNativeConstructOnly(val, r.builtin_newSet, r.getSetPrototype(), "Set", 0) + r.putSpeciesReturnThis(o) + + return o +} + +func (r *Runtime) createSetIterProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.getIteratorPrototype(), classObject) + + o._putProp("next", r.newNativeFunc(r.setIterProto_next, "next", 0), true, false, true) + o._putSym(SymToStringTag, valueProp(asciiString(classSetIterator), false, false, true)) + + return o +} + +func (r *Runtime) getSetIteratorPrototype() *Object { + var o *Object + if o = r.global.SetIteratorPrototype; o == nil { + o = &Object{runtime: r} + r.global.SetIteratorPrototype = o + o.self = r.createSetIterProto(o) + } + return o +} + +func (r *Runtime) getSetPrototype() *Object { + ret := r.global.SetPrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.SetPrototype = ret + ret.self = r.createSetProto(ret) + } + return ret +} + +func (r *Runtime) getSet() *Object { + ret := r.global.Set + if ret == nil { + ret = &Object{runtime: r} + r.global.Set = ret + ret.self = r.createSet(ret) + } + return ret +} diff --git a/goja/builtin_set_test.go b/goja/builtin_set_test.go new file mode 100644 index 0000000..bd5fc6a --- /dev/null +++ b/goja/builtin_set_test.go @@ -0,0 +1,180 @@ +package goja + +import ( + "fmt" + "strings" + "testing" +) + +func TestSetEvilIterator(t *testing.T) { + const SCRIPT = ` + var o = {}; + o[Symbol.iterator] = function() { + return { + next: function() { + if (!this.flag) { + this.flag = true; + return {}; + } + return {done: true}; + } + } + } + new Set(o); + undefined; + ` + testScript(SCRIPT, _undefined, t) +} + +func ExampleRuntime_ExportTo_setToMap() { + vm := New() + s, err := vm.RunString(` + new Set([1, 2, 3]) + `) + if err != nil { + panic(err) + } + m := make(map[int]struct{}) + err = vm.ExportTo(s, &m) + if err != nil { + panic(err) + } + fmt.Println(m) + // Output: map[1:{} 2:{} 3:{}] +} + +func ExampleRuntime_ExportTo_setToSlice() { + vm := New() + s, err := vm.RunString(` + new Set([1, 2, 3]) + `) + if err != nil { + panic(err) + } + var a []int + err = vm.ExportTo(s, &a) + if err != nil { + panic(err) + } + fmt.Println(a) + // Output: [1 2 3] +} + +func TestSetExportToSliceCircular(t *testing.T) { + vm := New() + s, err := vm.RunString(` + let s = new Set(); + s.add(s); + s; + `) + if err != nil { + t.Fatal(err) + } + var a []Value + err = vm.ExportTo(s, &a) + if err != nil { + t.Fatal(err) + } + if len(a) != 1 { + t.Fatalf("len: %d", len(a)) + } + if a[0] != s { + t.Fatalf("a: %v", a) + } +} + +func TestSetExportToArrayMismatchedLengths(t *testing.T) { + vm := New() + s, err := vm.RunString(` + new Set([1, 2]) + `) + if err != nil { + panic(err) + } + var s1 [3]int + err = vm.ExportTo(s, &s1) + if err == nil { + t.Fatal("expected error") + } + if msg := err.Error(); !strings.Contains(msg, "lengths mismatch") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestSetExportToNilMap(t *testing.T) { + vm := New() + var m map[int]interface{} + res, err := vm.RunString("new Set([1])") + if err != nil { + t.Fatal(err) + } + err = vm.ExportTo(res, &m) + if err != nil { + t.Fatal(err) + } + if len(m) != 1 { + t.Fatal(m) + } + if _, exists := m[1]; !exists { + t.Fatal(m) + } +} + +func TestSetExportToNonNilMap(t *testing.T) { + vm := New() + m := map[int]interface{}{ + 2: true, + } + res, err := vm.RunString("new Set([1])") + if err != nil { + t.Fatal(err) + } + err = vm.ExportTo(res, &m) + if err != nil { + t.Fatal(err) + } + if len(m) != 1 { + t.Fatal(m) + } + if _, exists := m[1]; !exists { + t.Fatal(m) + } +} + +func TestSetGetAdderGetIteratorOrder(t *testing.T) { + const SCRIPT = ` + let getterCalled = 0; + + class S extends Set { + get add() { + getterCalled++; + return null; + } + } + + let getIteratorCalled = 0; + + let iterable = {}; + iterable[Symbol.iterator] = () => { + getIteratorCalled++ + return { + next: 1 + }; + } + + let thrown = false; + + try { + new S(iterable); + } catch (e) { + if (e instanceof TypeError) { + thrown = true; + } else { + throw e; + } + } + + thrown && getterCalled === 1 && getIteratorCalled === 0; + ` + testScript(SCRIPT, valueTrue, t) +} diff --git a/goja/builtin_string.go b/goja/builtin_string.go new file mode 100644 index 0000000..067c615 --- /dev/null +++ b/goja/builtin_string.go @@ -0,0 +1,1112 @@ +package goja + +import ( + "math" + "strings" + "sync" + "unicode/utf16" + "unicode/utf8" + + "github.com/dop251/goja/unistring" + + "github.com/dop251/goja/parser" + "golang.org/x/text/collate" + "golang.org/x/text/language" + "golang.org/x/text/unicode/norm" +) + +func (r *Runtime) collator() *collate.Collator { + collator := r._collator + if collator == nil { + collator = collate.New(language.Und) + r._collator = collator + } + return collator +} + +func toString(arg Value) String { + if s, ok := arg.(String); ok { + return s + } + if s, ok := arg.(*Symbol); ok { + return s.descriptiveString() + } + return arg.toString() +} + +func (r *Runtime) builtin_String(call FunctionCall) Value { + if len(call.Arguments) > 0 { + return toString(call.Arguments[0]) + } else { + return stringEmpty + } +} + +func (r *Runtime) _newString(s String, proto *Object) *Object { + v := &Object{runtime: r} + + o := &stringObject{} + o.class = classString + o.val = v + o.extensible = true + v.self = o + o.prototype = proto + if s != nil { + o.value = s + } + o.init() + return v +} + +func (r *Runtime) builtin_newString(args []Value, proto *Object) *Object { + var s String + if len(args) > 0 { + s = args[0].toString() + } else { + s = stringEmpty + } + return r._newString(s, proto) +} + +func (r *Runtime) stringproto_toStringValueOf(this Value, funcName string) Value { + if str, ok := this.(String); ok { + return str + } + if obj, ok := this.(*Object); ok { + if strObj, ok := obj.self.(*stringObject); ok { + return strObj.value + } + if reflectObj, ok := obj.self.(*objectGoReflect); ok && reflectObj.class == classString { + if toString := reflectObj.toString; toString != nil { + return toString() + } + if valueOf := reflectObj.valueOf; valueOf != nil { + return valueOf() + } + } + if obj == r.global.StringPrototype { + return stringEmpty + } + } + r.typeErrorResult(true, "String.prototype.%s is called on incompatible receiver", funcName) + return nil +} + +func (r *Runtime) stringproto_toString(call FunctionCall) Value { + return r.stringproto_toStringValueOf(call.This, "toString") +} + +func (r *Runtime) stringproto_valueOf(call FunctionCall) Value { + return r.stringproto_toStringValueOf(call.This, "valueOf") +} + +func (r *Runtime) stringproto_iterator(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + return r.createStringIterator(call.This.toString()) +} + +func (r *Runtime) string_fromcharcode(call FunctionCall) Value { + b := make([]byte, len(call.Arguments)) + for i, arg := range call.Arguments { + chr := toUint16(arg) + if chr >= utf8.RuneSelf { + bb := make([]uint16, len(call.Arguments)+1) + bb[0] = unistring.BOM + bb1 := bb[1:] + for j := 0; j < i; j++ { + bb1[j] = uint16(b[j]) + } + bb1[i] = chr + i++ + for j, arg := range call.Arguments[i:] { + bb1[i+j] = toUint16(arg) + } + return unicodeString(bb) + } + b[i] = byte(chr) + } + + return asciiString(b) +} + +func (r *Runtime) string_fromcodepoint(call FunctionCall) Value { + var sb StringBuilder + for _, arg := range call.Arguments { + num := arg.ToNumber() + var c rune + if numInt, ok := num.(valueInt); ok { + if numInt < 0 || numInt > utf8.MaxRune { + panic(r.newError(r.getRangeError(), "Invalid code point %d", numInt)) + } + c = rune(numInt) + } else { + panic(r.newError(r.getRangeError(), "Invalid code point %s", num)) + } + sb.WriteRune(c) + } + return sb.String() +} + +func (r *Runtime) string_raw(call FunctionCall) Value { + cooked := call.Argument(0).ToObject(r) + raw := nilSafe(cooked.self.getStr("raw", nil)).ToObject(r) + literalSegments := toLength(raw.self.getStr("length", nil)) + if literalSegments <= 0 { + return stringEmpty + } + var stringElements StringBuilder + nextIndex := int64(0) + numberOfSubstitutions := int64(len(call.Arguments) - 1) + for { + nextSeg := nilSafe(raw.self.getIdx(valueInt(nextIndex), nil)).toString() + stringElements.WriteString(nextSeg) + if nextIndex+1 == literalSegments { + return stringElements.String() + } + if nextIndex < numberOfSubstitutions { + stringElements.WriteString(nilSafe(call.Arguments[nextIndex+1]).toString()) + } + nextIndex++ + } +} + +func (r *Runtime) stringproto_at(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + pos := call.Argument(0).ToInteger() + length := int64(s.Length()) + if pos < 0 { + pos = length + pos + } + if pos >= length || pos < 0 { + return _undefined + } + return s.Substring(int(pos), int(pos+1)) +} + +func (r *Runtime) stringproto_charAt(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + pos := call.Argument(0).ToInteger() + if pos < 0 || pos >= int64(s.Length()) { + return stringEmpty + } + return s.Substring(int(pos), int(pos+1)) +} + +func (r *Runtime) stringproto_charCodeAt(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + pos := call.Argument(0).ToInteger() + if pos < 0 || pos >= int64(s.Length()) { + return _NaN + } + return intToValue(int64(s.CharAt(toIntStrict(pos)) & 0xFFFF)) +} + +func (r *Runtime) stringproto_codePointAt(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + p := call.Argument(0).ToInteger() + size := s.Length() + if p < 0 || p >= int64(size) { + return _undefined + } + pos := toIntStrict(p) + first := s.CharAt(pos) + if isUTF16FirstSurrogate(first) { + pos++ + if pos < size { + second := s.CharAt(pos) + if isUTF16SecondSurrogate(second) { + return intToValue(int64(utf16.DecodeRune(rune(first), rune(second)))) + } + } + } + return intToValue(int64(first & 0xFFFF)) +} + +func (r *Runtime) stringproto_concat(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + strs := make([]String, len(call.Arguments)+1) + a, u := devirtualizeString(call.This.toString()) + allAscii := true + totalLen := 0 + if u == nil { + strs[0] = a + totalLen = len(a) + } else { + strs[0] = u + totalLen = u.Length() + allAscii = false + } + for i, arg := range call.Arguments { + a, u := devirtualizeString(arg.toString()) + if u != nil { + allAscii = false + totalLen += u.Length() + strs[i+1] = u + } else { + totalLen += a.Length() + strs[i+1] = a + } + } + + if allAscii { + var buf strings.Builder + buf.Grow(totalLen) + for _, s := range strs { + buf.WriteString(s.String()) + } + return asciiString(buf.String()) + } else { + buf := make([]uint16, totalLen+1) + buf[0] = unistring.BOM + pos := 1 + for _, s := range strs { + switch s := s.(type) { + case asciiString: + for i := 0; i < len(s); i++ { + buf[pos] = uint16(s[i]) + pos++ + } + case unicodeString: + copy(buf[pos:], s[1:]) + pos += s.Length() + } + } + return unicodeString(buf) + } +} + +func (r *Runtime) stringproto_endsWith(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + searchString := call.Argument(0) + if isRegexp(searchString) { + panic(r.NewTypeError("First argument to String.prototype.endsWith must not be a regular expression")) + } + searchStr := searchString.toString() + l := int64(s.Length()) + var pos int64 + if posArg := call.Argument(1); posArg != _undefined { + pos = posArg.ToInteger() + } else { + pos = l + } + end := toIntStrict(min(max(pos, 0), l)) + searchLength := searchStr.Length() + start := end - searchLength + if start < 0 { + return valueFalse + } + for i := 0; i < searchLength; i++ { + if s.CharAt(start+i) != searchStr.CharAt(i) { + return valueFalse + } + } + return valueTrue +} + +func (r *Runtime) stringproto_includes(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + searchString := call.Argument(0) + if isRegexp(searchString) { + panic(r.NewTypeError("First argument to String.prototype.includes must not be a regular expression")) + } + searchStr := searchString.toString() + var pos int64 + if posArg := call.Argument(1); posArg != _undefined { + pos = posArg.ToInteger() + } else { + pos = 0 + } + start := toIntStrict(min(max(pos, 0), int64(s.Length()))) + if s.index(searchStr, start) != -1 { + return valueTrue + } + return valueFalse +} + +func (r *Runtime) stringproto_indexOf(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + value := call.This.toString() + target := call.Argument(0).toString() + pos := call.Argument(1).ToNumber().ToInteger() + + if pos < 0 { + pos = 0 + } else { + l := int64(value.Length()) + if pos > l { + pos = l + } + } + + return intToValue(int64(value.index(target, toIntStrict(pos)))) +} + +func (r *Runtime) stringproto_lastIndexOf(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + value := call.This.toString() + target := call.Argument(0).toString() + numPos := call.Argument(1).ToNumber() + + var pos int64 + if f, ok := numPos.(valueFloat); ok && math.IsNaN(float64(f)) { + pos = int64(value.Length()) + } else { + pos = numPos.ToInteger() + if pos < 0 { + pos = 0 + } else { + l := int64(value.Length()) + if pos > l { + pos = l + } + } + } + + return intToValue(int64(value.lastIndex(target, toIntStrict(pos)))) +} + +func (r *Runtime) stringproto_localeCompare(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + this := norm.NFD.String(call.This.toString().String()) + that := norm.NFD.String(call.Argument(0).toString().String()) + return intToValue(int64(r.collator().CompareString(this, that))) +} + +func (r *Runtime) stringproto_match(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + regexp := call.Argument(0) + if regexp != _undefined && regexp != _null { + if matcher := toMethod(r.getV(regexp, SymMatch)); matcher != nil { + return matcher(FunctionCall{ + This: regexp, + Arguments: []Value{call.This}, + }) + } + } + + var rx *regexpObject + if regexp, ok := regexp.(*Object); ok { + rx, _ = regexp.self.(*regexpObject) + } + + if rx == nil { + rx = r.newRegExp(regexp, nil, r.getRegExpPrototype()) + } + + if matcher, ok := r.toObject(rx.getSym(SymMatch, nil)).self.assertCallable(); ok { + return matcher(FunctionCall{ + This: rx.val, + Arguments: []Value{call.This.toString()}, + }) + } + + panic(r.NewTypeError("RegExp matcher is not a function")) +} + +func (r *Runtime) stringproto_matchAll(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + regexp := call.Argument(0) + if regexp != _undefined && regexp != _null { + if isRegexp(regexp) { + if o, ok := regexp.(*Object); ok { + flags := nilSafe(o.self.getStr("flags", nil)) + r.checkObjectCoercible(flags) + if !strings.Contains(flags.toString().String(), "g") { + panic(r.NewTypeError("RegExp doesn't have global flag set")) + } + } + } + if matcher := toMethod(r.getV(regexp, SymMatchAll)); matcher != nil { + return matcher(FunctionCall{ + This: regexp, + Arguments: []Value{call.This}, + }) + } + } + + rx := r.newRegExp(regexp, asciiString("g"), r.getRegExpPrototype()) + + if matcher, ok := r.toObject(rx.getSym(SymMatchAll, nil)).self.assertCallable(); ok { + return matcher(FunctionCall{ + This: rx.val, + Arguments: []Value{call.This.toString()}, + }) + } + + panic(r.NewTypeError("RegExp matcher is not a function")) +} + +func (r *Runtime) stringproto_normalize(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + var form string + if formArg := call.Argument(0); formArg != _undefined { + form = formArg.toString().toString().String() + } else { + form = "NFC" + } + var f norm.Form + switch form { + case "NFC": + f = norm.NFC + case "NFD": + f = norm.NFD + case "NFKC": + f = norm.NFKC + case "NFKD": + f = norm.NFKD + default: + panic(r.newError(r.getRangeError(), "The normalization form should be one of NFC, NFD, NFKC, NFKD")) + } + + switch s := s.(type) { + case asciiString: + return s + case unicodeString: + ss := s.String() + return newStringValue(f.String(ss)) + case *importedString: + if s.scanned && s.u == nil { + return asciiString(s.s) + } + return newStringValue(f.String(s.s)) + default: + panic(unknownStringTypeErr(s)) + } +} + +func (r *Runtime) _stringPad(call FunctionCall, start bool) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + maxLength := toLength(call.Argument(0)) + stringLength := int64(s.Length()) + if maxLength <= stringLength { + return s + } + strAscii, strUnicode := devirtualizeString(s) + var filler String + var fillerAscii asciiString + var fillerUnicode unicodeString + if fillString := call.Argument(1); fillString != _undefined { + filler = fillString.toString() + if filler.Length() == 0 { + return s + } + fillerAscii, fillerUnicode = devirtualizeString(filler) + } else { + fillerAscii = " " + filler = fillerAscii + } + remaining := toIntStrict(maxLength - stringLength) + if fillerUnicode == nil && strUnicode == nil { + fl := fillerAscii.Length() + var sb strings.Builder + sb.Grow(toIntStrict(maxLength)) + if !start { + sb.WriteString(string(strAscii)) + } + for remaining >= fl { + sb.WriteString(string(fillerAscii)) + remaining -= fl + } + if remaining > 0 { + sb.WriteString(string(fillerAscii[:remaining])) + } + if start { + sb.WriteString(string(strAscii)) + } + return asciiString(sb.String()) + } + var sb unicodeStringBuilder + sb.ensureStarted(toIntStrict(maxLength)) + if !start { + sb.writeString(s) + } + fl := filler.Length() + for remaining >= fl { + sb.writeString(filler) + remaining -= fl + } + if remaining > 0 { + sb.writeString(filler.Substring(0, remaining)) + } + if start { + sb.writeString(s) + } + + return sb.String() +} + +func (r *Runtime) stringproto_padEnd(call FunctionCall) Value { + return r._stringPad(call, false) +} + +func (r *Runtime) stringproto_padStart(call FunctionCall) Value { + return r._stringPad(call, true) +} + +func (r *Runtime) stringproto_repeat(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + n := call.Argument(0).ToNumber() + if n == _positiveInf { + panic(r.newError(r.getRangeError(), "Invalid count value")) + } + numInt := n.ToInteger() + if numInt < 0 { + panic(r.newError(r.getRangeError(), "Invalid count value")) + } + if numInt == 0 || s.Length() == 0 { + return stringEmpty + } + num := toIntStrict(numInt) + a, u := devirtualizeString(s) + if u == nil { + var sb strings.Builder + sb.Grow(len(a) * num) + for i := 0; i < num; i++ { + sb.WriteString(string(a)) + } + return asciiString(sb.String()) + } + + var sb unicodeStringBuilder + sb.Grow(u.Length() * num) + for i := 0; i < num; i++ { + sb.writeUnicodeString(u) + } + return sb.String() +} + +func getReplaceValue(replaceValue Value) (str String, rcall func(FunctionCall) Value) { + if replaceValue, ok := replaceValue.(*Object); ok { + if c, ok := replaceValue.self.assertCallable(); ok { + rcall = c + return + } + } + str = replaceValue.toString() + return +} + +func stringReplace(s String, found [][]int, newstring String, rcall func(FunctionCall) Value) Value { + if len(found) == 0 { + return s + } + + a, u := devirtualizeString(s) + + var buf StringBuilder + + lastIndex := 0 + lengthS := s.Length() + if rcall != nil { + for _, item := range found { + if item[0] != lastIndex { + buf.WriteSubstring(s, lastIndex, item[0]) + } + matchCount := len(item) / 2 + argumentList := make([]Value, matchCount+2) + for index := 0; index < matchCount; index++ { + offset := 2 * index + if item[offset] != -1 { + if u == nil { + argumentList[index] = a[item[offset]:item[offset+1]] + } else { + argumentList[index] = u.Substring(item[offset], item[offset+1]) + } + } else { + argumentList[index] = _undefined + } + } + argumentList[matchCount] = valueInt(item[0]) + argumentList[matchCount+1] = s + replacement := rcall(FunctionCall{ + This: _undefined, + Arguments: argumentList, + }).toString() + buf.WriteString(replacement) + lastIndex = item[1] + } + } else { + for _, item := range found { + if item[0] != lastIndex { + buf.WriteString(s.Substring(lastIndex, item[0])) + } + matchCount := len(item) / 2 + writeSubstitution(s, item[0], matchCount, func(idx int) String { + if item[idx*2] != -1 { + if u == nil { + return a[item[idx*2]:item[idx*2+1]] + } + return u.Substring(item[idx*2], item[idx*2+1]) + } + return stringEmpty + }, newstring, &buf) + lastIndex = item[1] + } + } + + if lastIndex != lengthS { + buf.WriteString(s.Substring(lastIndex, lengthS)) + } + + return buf.String() +} + +func (r *Runtime) stringproto_replace(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + searchValue := call.Argument(0) + replaceValue := call.Argument(1) + if searchValue != _undefined && searchValue != _null { + if replacer := toMethod(r.getV(searchValue, SymReplace)); replacer != nil { + return replacer(FunctionCall{ + This: searchValue, + Arguments: []Value{call.This, replaceValue}, + }) + } + } + + s := call.This.toString() + var found [][]int + searchStr := searchValue.toString() + pos := s.index(searchStr, 0) + if pos != -1 { + found = append(found, []int{pos, pos + searchStr.Length()}) + } + + str, rcall := getReplaceValue(replaceValue) + return stringReplace(s, found, str, rcall) +} + +func (r *Runtime) stringproto_replaceAll(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + searchValue := call.Argument(0) + replaceValue := call.Argument(1) + if searchValue != _undefined && searchValue != _null { + if isRegexp(searchValue) { + if o, ok := searchValue.(*Object); ok { + flags := nilSafe(o.self.getStr("flags", nil)) + r.checkObjectCoercible(flags) + if !strings.Contains(flags.toString().String(), "g") { + panic(r.NewTypeError("String.prototype.replaceAll called with a non-global RegExp argument")) + } + } + } + if replacer := toMethod(r.getV(searchValue, SymReplace)); replacer != nil { + return replacer(FunctionCall{ + This: searchValue, + Arguments: []Value{call.This, replaceValue}, + }) + } + } + + s := call.This.toString() + var found [][]int + searchStr := searchValue.toString() + searchLength := searchStr.Length() + advanceBy := toIntStrict(max(1, int64(searchLength))) + + pos := s.index(searchStr, 0) + for pos != -1 { + found = append(found, []int{pos, pos + searchLength}) + pos = s.index(searchStr, pos+advanceBy) + } + + str, rcall := getReplaceValue(replaceValue) + return stringReplace(s, found, str, rcall) +} + +func (r *Runtime) stringproto_search(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + regexp := call.Argument(0) + if regexp != _undefined && regexp != _null { + if searcher := toMethod(r.getV(regexp, SymSearch)); searcher != nil { + return searcher(FunctionCall{ + This: regexp, + Arguments: []Value{call.This}, + }) + } + } + + var rx *regexpObject + if regexp, ok := regexp.(*Object); ok { + rx, _ = regexp.self.(*regexpObject) + } + + if rx == nil { + rx = r.newRegExp(regexp, nil, r.getRegExpPrototype()) + } + + if searcher, ok := r.toObject(rx.getSym(SymSearch, nil)).self.assertCallable(); ok { + return searcher(FunctionCall{ + This: rx.val, + Arguments: []Value{call.This.toString()}, + }) + } + + panic(r.NewTypeError("RegExp searcher is not a function")) +} + +func (r *Runtime) stringproto_slice(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + + l := int64(s.Length()) + start := call.Argument(0).ToInteger() + var end int64 + if arg1 := call.Argument(1); arg1 != _undefined { + end = arg1.ToInteger() + } else { + end = l + } + + if start < 0 { + start += l + if start < 0 { + start = 0 + } + } else { + if start > l { + start = l + } + } + + if end < 0 { + end += l + if end < 0 { + end = 0 + } + } else { + if end > l { + end = l + } + } + + if end > start { + return s.Substring(int(start), int(end)) + } + return stringEmpty +} + +func (r *Runtime) stringproto_split(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + separatorValue := call.Argument(0) + limitValue := call.Argument(1) + if separatorValue != _undefined && separatorValue != _null { + if splitter := toMethod(r.getV(separatorValue, SymSplit)); splitter != nil { + return splitter(FunctionCall{ + This: separatorValue, + Arguments: []Value{call.This, limitValue}, + }) + } + } + s := call.This.toString() + + limit := -1 + if limitValue != _undefined { + limit = int(toUint32(limitValue)) + } + + separatorValue = separatorValue.ToString() + + if limit == 0 { + return r.newArrayValues(nil) + } + + if separatorValue == _undefined { + return r.newArrayValues([]Value{s}) + } + + separator := separatorValue.String() + + str := s.String() + splitLimit := limit + if limit > 0 { + splitLimit = limit + 1 + } + + // TODO handle invalid UTF-16 + split := strings.SplitN(str, separator, splitLimit) + + if limit > 0 && len(split) > limit { + split = split[:limit] + } + + valueArray := make([]Value, len(split)) + for index, value := range split { + valueArray[index] = newStringValue(value) + } + + return r.newArrayValues(valueArray) +} + +func (r *Runtime) stringproto_startsWith(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + searchString := call.Argument(0) + if isRegexp(searchString) { + panic(r.NewTypeError("First argument to String.prototype.startsWith must not be a regular expression")) + } + searchStr := searchString.toString() + l := int64(s.Length()) + var pos int64 + if posArg := call.Argument(1); posArg != _undefined { + pos = posArg.ToInteger() + } + start := toIntStrict(min(max(pos, 0), l)) + searchLength := searchStr.Length() + if int64(searchLength+start) > l { + return valueFalse + } + for i := 0; i < searchLength; i++ { + if s.CharAt(start+i) != searchStr.CharAt(i) { + return valueFalse + } + } + return valueTrue +} + +func (r *Runtime) stringproto_substring(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + + l := int64(s.Length()) + intStart := call.Argument(0).ToInteger() + var intEnd int64 + if end := call.Argument(1); end != _undefined { + intEnd = end.ToInteger() + } else { + intEnd = l + } + if intStart < 0 { + intStart = 0 + } else if intStart > l { + intStart = l + } + + if intEnd < 0 { + intEnd = 0 + } else if intEnd > l { + intEnd = l + } + + if intStart > intEnd { + intStart, intEnd = intEnd, intStart + } + + return s.Substring(int(intStart), int(intEnd)) +} + +func (r *Runtime) stringproto_toLowerCase(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + + return s.toLower() +} + +func (r *Runtime) stringproto_toUpperCase(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + + return s.toUpper() +} + +func (r *Runtime) stringproto_trim(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + + // TODO handle invalid UTF-16 + return newStringValue(strings.Trim(s.String(), parser.WhitespaceChars)) +} + +func (r *Runtime) stringproto_trimEnd(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + + // TODO handle invalid UTF-16 + return newStringValue(strings.TrimRight(s.String(), parser.WhitespaceChars)) +} + +func (r *Runtime) stringproto_trimStart(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + + // TODO handle invalid UTF-16 + return newStringValue(strings.TrimLeft(s.String(), parser.WhitespaceChars)) +} + +func (r *Runtime) stringproto_substr(call FunctionCall) Value { + r.checkObjectCoercible(call.This) + s := call.This.toString() + start := call.Argument(0).ToInteger() + var length int64 + sl := int64(s.Length()) + if arg := call.Argument(1); arg != _undefined { + length = arg.ToInteger() + } else { + length = sl + } + + if start < 0 { + start = max(sl+start, 0) + } + + length = min(max(length, 0), sl-start) + if length <= 0 { + return stringEmpty + } + + return s.Substring(int(start), int(start+length)) +} + +func (r *Runtime) stringIterProto_next(call FunctionCall) Value { + thisObj := r.toObject(call.This) + if iter, ok := thisObj.self.(*stringIterObject); ok { + return iter.next() + } + panic(r.NewTypeError("Method String Iterator.prototype.next called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) +} + +func (r *Runtime) createStringIterProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.getIteratorPrototype(), classObject) + + o._putProp("next", r.newNativeFunc(r.stringIterProto_next, "next", 0), true, false, true) + o._putSym(SymToStringTag, valueProp(asciiString(classStringIterator), false, false, true)) + + return o +} + +func (r *Runtime) getStringIteratorPrototype() *Object { + var o *Object + if o = r.global.StringIteratorPrototype; o == nil { + o = &Object{runtime: r} + r.global.StringIteratorPrototype = o + o.self = r.createStringIterProto(o) + } + return o +} + +func createStringProtoTemplate() *objectTemplate { + t := newObjectTemplate() + t.protoFactory = func(r *Runtime) *Object { + return r.global.ObjectPrototype + } + + t.putStr("length", func(r *Runtime) Value { return valueProp(intToValue(0), false, false, false) }) + + t.putStr("constructor", func(r *Runtime) Value { return valueProp(r.getString(), true, false, true) }) + + t.putStr("at", func(r *Runtime) Value { return r.methodProp(r.stringproto_at, "at", 1) }) + t.putStr("charAt", func(r *Runtime) Value { return r.methodProp(r.stringproto_charAt, "charAt", 1) }) + t.putStr("charCodeAt", func(r *Runtime) Value { return r.methodProp(r.stringproto_charCodeAt, "charCodeAt", 1) }) + t.putStr("codePointAt", func(r *Runtime) Value { return r.methodProp(r.stringproto_codePointAt, "codePointAt", 1) }) + t.putStr("concat", func(r *Runtime) Value { return r.methodProp(r.stringproto_concat, "concat", 1) }) + t.putStr("endsWith", func(r *Runtime) Value { return r.methodProp(r.stringproto_endsWith, "endsWith", 1) }) + t.putStr("includes", func(r *Runtime) Value { return r.methodProp(r.stringproto_includes, "includes", 1) }) + t.putStr("indexOf", func(r *Runtime) Value { return r.methodProp(r.stringproto_indexOf, "indexOf", 1) }) + t.putStr("lastIndexOf", func(r *Runtime) Value { return r.methodProp(r.stringproto_lastIndexOf, "lastIndexOf", 1) }) + t.putStr("localeCompare", func(r *Runtime) Value { return r.methodProp(r.stringproto_localeCompare, "localeCompare", 1) }) + t.putStr("match", func(r *Runtime) Value { return r.methodProp(r.stringproto_match, "match", 1) }) + t.putStr("matchAll", func(r *Runtime) Value { return r.methodProp(r.stringproto_matchAll, "matchAll", 1) }) + t.putStr("normalize", func(r *Runtime) Value { return r.methodProp(r.stringproto_normalize, "normalize", 0) }) + t.putStr("padEnd", func(r *Runtime) Value { return r.methodProp(r.stringproto_padEnd, "padEnd", 1) }) + t.putStr("padStart", func(r *Runtime) Value { return r.methodProp(r.stringproto_padStart, "padStart", 1) }) + t.putStr("repeat", func(r *Runtime) Value { return r.methodProp(r.stringproto_repeat, "repeat", 1) }) + t.putStr("replace", func(r *Runtime) Value { return r.methodProp(r.stringproto_replace, "replace", 2) }) + t.putStr("replaceAll", func(r *Runtime) Value { return r.methodProp(r.stringproto_replaceAll, "replaceAll", 2) }) + t.putStr("search", func(r *Runtime) Value { return r.methodProp(r.stringproto_search, "search", 1) }) + t.putStr("slice", func(r *Runtime) Value { return r.methodProp(r.stringproto_slice, "slice", 2) }) + t.putStr("split", func(r *Runtime) Value { return r.methodProp(r.stringproto_split, "split", 2) }) + t.putStr("startsWith", func(r *Runtime) Value { return r.methodProp(r.stringproto_startsWith, "startsWith", 1) }) + t.putStr("substring", func(r *Runtime) Value { return r.methodProp(r.stringproto_substring, "substring", 2) }) + t.putStr("toLocaleLowerCase", func(r *Runtime) Value { return r.methodProp(r.stringproto_toLowerCase, "toLocaleLowerCase", 0) }) + t.putStr("toLocaleUpperCase", func(r *Runtime) Value { return r.methodProp(r.stringproto_toUpperCase, "toLocaleUpperCase", 0) }) + t.putStr("toLowerCase", func(r *Runtime) Value { return r.methodProp(r.stringproto_toLowerCase, "toLowerCase", 0) }) + t.putStr("toString", func(r *Runtime) Value { return r.methodProp(r.stringproto_toString, "toString", 0) }) + t.putStr("toUpperCase", func(r *Runtime) Value { return r.methodProp(r.stringproto_toUpperCase, "toUpperCase", 0) }) + t.putStr("trim", func(r *Runtime) Value { return r.methodProp(r.stringproto_trim, "trim", 0) }) + t.putStr("trimEnd", func(r *Runtime) Value { return valueProp(r.getStringproto_trimEnd(), true, false, true) }) + t.putStr("trimStart", func(r *Runtime) Value { return valueProp(r.getStringproto_trimStart(), true, false, true) }) + t.putStr("trimRight", func(r *Runtime) Value { return valueProp(r.getStringproto_trimEnd(), true, false, true) }) + t.putStr("trimLeft", func(r *Runtime) Value { return valueProp(r.getStringproto_trimStart(), true, false, true) }) + t.putStr("valueOf", func(r *Runtime) Value { return r.methodProp(r.stringproto_valueOf, "valueOf", 0) }) + + // Annex B + t.putStr("substr", func(r *Runtime) Value { return r.methodProp(r.stringproto_substr, "substr", 2) }) + + t.putSym(SymIterator, func(r *Runtime) Value { + return valueProp(r.newNativeFunc(r.stringproto_iterator, "[Symbol.iterator]", 0), true, false, true) + }) + + return t +} + +func (r *Runtime) getStringproto_trimEnd() *Object { + ret := r.global.stringproto_trimEnd + if ret == nil { + ret = r.newNativeFunc(r.stringproto_trimEnd, "trimEnd", 0) + r.global.stringproto_trimEnd = ret + } + return ret +} + +func (r *Runtime) getStringproto_trimStart() *Object { + ret := r.global.stringproto_trimStart + if ret == nil { + ret = r.newNativeFunc(r.stringproto_trimStart, "trimStart", 0) + r.global.stringproto_trimStart = ret + } + return ret +} + +func (r *Runtime) getStringSingleton() *stringObject { + ret := r.stringSingleton + if ret == nil { + ret = r.builtin_new(r.getString(), nil).self.(*stringObject) + r.stringSingleton = ret + } + return ret +} + +func (r *Runtime) getString() *Object { + ret := r.global.String + if ret == nil { + ret = &Object{runtime: r} + r.global.String = ret + proto := r.getStringPrototype() + o := r.newNativeFuncAndConstruct(ret, r.builtin_String, r.wrapNativeConstruct(r.builtin_newString, ret, proto), proto, "String", intToValue(1)) + ret.self = o + o._putProp("fromCharCode", r.newNativeFunc(r.string_fromcharcode, "fromCharCode", 1), true, false, true) + o._putProp("fromCodePoint", r.newNativeFunc(r.string_fromcodepoint, "fromCodePoint", 1), true, false, true) + o._putProp("raw", r.newNativeFunc(r.string_raw, "raw", 1), true, false, true) + } + return ret +} + +var stringProtoTemplate *objectTemplate +var stringProtoTemplateOnce sync.Once + +func getStringProtoTemplate() *objectTemplate { + stringProtoTemplateOnce.Do(func() { + stringProtoTemplate = createStringProtoTemplate() + }) + return stringProtoTemplate +} + +func (r *Runtime) getStringPrototype() *Object { + ret := r.global.StringPrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.StringPrototype = ret + o := r.newTemplatedObject(getStringProtoTemplate(), ret) + o.class = classString + } + return ret +} diff --git a/goja/builtin_string_test.go b/goja/builtin_string_test.go new file mode 100644 index 0000000..2c217f0 --- /dev/null +++ b/goja/builtin_string_test.go @@ -0,0 +1,288 @@ +package goja + +import "testing" + +func TestSubstr(t *testing.T) { + const SCRIPT = ` +assert.sameValue('abc'.substr(0, false), '', 'start: 0, length: false'); +assert.sameValue('abc'.substr(1, false), '', 'start: 1, length: false'); +assert.sameValue('abc'.substr(2, false), '', 'start: 2, length: false'); +assert.sameValue('abc'.substr(3, false), '', 'start: 3, length: false'); + +assert.sameValue('abc'.substr(0, NaN), '', 'start: 0, length: NaN'); +assert.sameValue('abc'.substr(1, NaN), '', 'start: 1, length: NaN'); +assert.sameValue('abc'.substr(2, NaN), '', 'start: 2, length: NaN'); +assert.sameValue('abc'.substr(3, NaN), '', 'start: 3, length: NaN'); + +assert.sameValue('abc'.substr(0, ''), '', 'start: 0, length: ""'); +assert.sameValue('abc'.substr(1, ''), '', 'start: 1, length: ""'); +assert.sameValue('abc'.substr(2, ''), '', 'start: 2, length: ""'); +assert.sameValue('abc'.substr(3, ''), '', 'start: 3, length: ""'); + +assert.sameValue('abc'.substr(0, null), '', 'start: 0, length: null'); +assert.sameValue('abc'.substr(1, null), '', 'start: 1, length: null'); +assert.sameValue('abc'.substr(2, null), '', 'start: 2, length: null'); +assert.sameValue('abc'.substr(3, null), '', 'start: 3, length: null'); + +assert.sameValue('abc'.substr(0, -1), '', '0, -1'); +assert.sameValue('abc'.substr(0, -2), '', '0, -2'); +assert.sameValue('abc'.substr(0, -3), '', '0, -3'); +assert.sameValue('abc'.substr(0, -4), '', '0, -4'); + +assert.sameValue('abc'.substr(1, -1), '', '1, -1'); +assert.sameValue('abc'.substr(1, -2), '', '1, -2'); +assert.sameValue('abc'.substr(1, -3), '', '1, -3'); +assert.sameValue('abc'.substr(1, -4), '', '1, -4'); + +assert.sameValue('abc'.substr(2, -1), '', '2, -1'); +assert.sameValue('abc'.substr(2, -2), '', '2, -2'); +assert.sameValue('abc'.substr(2, -3), '', '2, -3'); +assert.sameValue('abc'.substr(2, -4), '', '2, -4'); + +assert.sameValue('abc'.substr(3, -1), '', '3, -1'); +assert.sameValue('abc'.substr(3, -2), '', '3, -2'); +assert.sameValue('abc'.substr(3, -3), '', '3, -3'); +assert.sameValue('abc'.substr(3, -4), '', '3, -4'); + +assert.sameValue('abc'.substr(0, 1), 'a', '0, 1'); +assert.sameValue('abc'.substr(0, 2), 'ab', '0, 1'); +assert.sameValue('abc'.substr(0, 3), 'abc', '0, 1'); +assert.sameValue('abc'.substr(0, 4), 'abc', '0, 1'); + +assert.sameValue('abc'.substr(1, 1), 'b', '1, 1'); +assert.sameValue('abc'.substr(1, 2), 'bc', '1, 1'); +assert.sameValue('abc'.substr(1, 3), 'bc', '1, 1'); +assert.sameValue('abc'.substr(1, 4), 'bc', '1, 1'); + +assert.sameValue('abc'.substr(2, 1), 'c', '2, 1'); +assert.sameValue('abc'.substr(2, 2), 'c', '2, 1'); +assert.sameValue('abc'.substr(2, 3), 'c', '2, 1'); +assert.sameValue('abc'.substr(2, 4), 'c', '2, 1'); + +assert.sameValue('abc'.substr(3, 1), '', '3, 1'); +assert.sameValue('abc'.substr(3, 2), '', '3, 1'); +assert.sameValue('abc'.substr(3, 3), '', '3, 1'); +assert.sameValue('abc'.substr(3, 4), '', '3, 1'); + +assert.sameValue('abc'.substr(0), 'abc', 'start: 0, length: unspecified'); +assert.sameValue('abc'.substr(1), 'bc', 'start: 1, length: unspecified'); +assert.sameValue('abc'.substr(2), 'c', 'start: 2, length: unspecified'); +assert.sameValue('abc'.substr(3), '', 'start: 3, length: unspecified'); + +assert.sameValue( + 'abc'.substr(0, undefined), 'abc', 'start: 0, length: undefined' +); +assert.sameValue( + 'abc'.substr(1, undefined), 'bc', 'start: 1, length: undefined' +); +assert.sameValue( + 'abc'.substr(2, undefined), 'c', 'start: 2, length: undefined' +); +assert.sameValue( + 'abc'.substr(3, undefined), '', 'start: 3, length: undefined' +); + +assert.sameValue('A—', String.fromCharCode(65, 0x2014)); + + ` + + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestStringMatchSym(t *testing.T) { + const SCRIPT = ` +function Prefix(p) { + this.p = p; +} + +Prefix.prototype[Symbol.match] = function(s) { + return s.substring(0, this.p.length) === this.p; +} + +var prefix1 = new Prefix("abc"); +var prefix2 = new Prefix("def"); + +"abc123".match(prefix1) === true && "abc123".match(prefix2) === false && +"def123".match(prefix1) === false && "def123".match(prefix2) === true; +` + testScript(SCRIPT, valueTrue, t) +} + +func TestStringMatchAllSym(t *testing.T) { + const SCRIPT = ` +function Prefix(p) { + this.p = p; +} + +Prefix.prototype[Symbol.matchAll] = function(s) { + return s.substring(0, this.p.length) === this.p; +} + +var prefix1 = new Prefix("abc"); +var prefix2 = new Prefix("def"); + +"abc123".matchAll(prefix1) === true && "abc123".matchAll(prefix2) === false && +"def123".matchAll(prefix1) === false && "def123".matchAll(prefix2) === true; +` + testScript(SCRIPT, valueTrue, t) +} + +func TestGenericSplitter(t *testing.T) { + const SCRIPT = ` +function MyRegexp(pattern, flags) { + if (pattern instanceof MyRegexp) { + pattern = pattern.wrapped; + } + this.wrapped = new RegExp(pattern, flags); +} + +MyRegexp.prototype.exec = function() { + return this.wrapped.exec.apply(this.wrapped, arguments); +} + +Object.defineProperty(MyRegexp.prototype, "lastIndex", { + get: function() { + return this.wrapped.lastIndex; + }, + set: function(v) { + this.wrapped.lastIndex = v; + } +}); + +Object.defineProperty(MyRegexp.prototype, "flags", { + get: function() { + return this.wrapped.flags; + } +}); + +MyRegexp[Symbol.species] = MyRegexp; +MyRegexp.prototype[Symbol.split] = RegExp.prototype[Symbol.split]; + +var r = new MyRegexp(/ /); +var res = "a b c".split(r); +res.length === 3 && res[0] === "a" && res[1] === "b" && res[2] === "c"; +` + testScript(SCRIPT, valueTrue, t) +} + +func TestStringIterSurrPair(t *testing.T) { + const SCRIPT = ` +var lo = '\uD834'; +var hi = '\uDF06'; +var pair = lo + hi; +var string = 'a' + pair + 'b' + lo + pair + hi + lo; +var iterator = string[Symbol.iterator](); +var result; + +result = iterator.next(); +if (result.value !== 'a') { + throw new Error("at 0: " + result.value); +} +result = iterator.next(); +if (result.value !== pair) { + throw new Error("at 1: " + result.value); +} + +` + testScript(SCRIPT, _undefined, t) +} + +func TestValueStringBuilder(t *testing.T) { + t.Run("substringASCII", func(t *testing.T) { + t.Parallel() + var sb StringBuilder + str := newStringValue("a\U00010000b") + sb.WriteSubstring(str, 0, 1) + res := sb.String() + if res != asciiString("a") { + t.Fatal(res) + } + }) + + t.Run("substringASCIIPure", func(t *testing.T) { + t.Parallel() + var sb StringBuilder + str := newStringValue("ab") + sb.WriteSubstring(str, 0, 1) + res := sb.String() + if res != asciiString("a") { + t.Fatal(res) + } + }) + + t.Run("substringUnicode", func(t *testing.T) { + t.Parallel() + var sb StringBuilder + str := newStringValue("a\U00010000b") + sb.WriteSubstring(str, 1, 3) + res := sb.String() + if !res.SameAs(unicodeStringFromRunes([]rune{0x10000})) { + t.Fatal(res) + } + }) + + t.Run("substringASCIIUnicode", func(t *testing.T) { + t.Parallel() + var sb StringBuilder + str := newStringValue("a\U00010000b") + sb.WriteSubstring(str, 0, 2) + res := sb.String() + if !res.SameAs(unicodeStringFromRunes([]rune{'a', 0xD800})) { + t.Fatal(res) + } + }) + + t.Run("substringUnicodeASCII", func(t *testing.T) { + t.Parallel() + var sb StringBuilder + str := newStringValue("a\U00010000b") + sb.WriteSubstring(str, 2, 4) + res := sb.String() + if !res.SameAs(unicodeStringFromRunes([]rune{0xDC00, 'b'})) { + t.Fatal(res) + } + }) + + t.Run("concatSubstringUnicodeASCII", func(t *testing.T) { + t.Parallel() + var sb StringBuilder + sb.WriteString(newStringValue("юникод")) + sb.WriteSubstring(asciiString(" ascii"), 0, 6) + if res := sb.String(); !res.SameAs(newStringValue("юникод ascii")) { + t.Fatal(res) + } + }) + + t.Run("concat_ASCII_importedASCII", func(t *testing.T) { + t.Parallel() + var sb StringBuilder + sb.WriteString(asciiString("ascii")) + sb.WriteString(&importedString{s: " imported_ascii1234567890"}) + s := sb.String() + if res, ok := s.(asciiString); !ok || res != "ascii imported_ascii1234567890" { + t.Fatal(s) + } + }) + + t.Run("concat_ASCII_importedUnicode", func(t *testing.T) { + t.Parallel() + var sb StringBuilder + sb.WriteString(asciiString("ascii")) + sb.WriteString(&importedString{s: " imported_юникод"}) + s := sb.String() + if res, ok := s.(unicodeString); !ok || !res.SameAs(newStringValue("ascii imported_юникод")) { + t.Fatal(s) + } + }) + +} + +func TestStringSplit(t *testing.T) { + const SCRIPT = ` + assert(compareArray("".split("#",2), [""])); + assert(compareArray("".split("#"), [""])); + assert(compareArray("".split("",2), [])); + assert(compareArray("".split(""), [])); +` + testScriptWithTestLib(SCRIPT, _undefined, t) +} diff --git a/goja/builtin_symbol.go b/goja/builtin_symbol.go new file mode 100644 index 0000000..8231b7b --- /dev/null +++ b/goja/builtin_symbol.go @@ -0,0 +1,177 @@ +package goja + +import "github.com/dop251/goja/unistring" + +var ( + SymHasInstance = newSymbol(asciiString("Symbol.hasInstance")) + SymIsConcatSpreadable = newSymbol(asciiString("Symbol.isConcatSpreadable")) + SymIterator = newSymbol(asciiString("Symbol.iterator")) + SymMatch = newSymbol(asciiString("Symbol.match")) + SymMatchAll = newSymbol(asciiString("Symbol.matchAll")) + SymReplace = newSymbol(asciiString("Symbol.replace")) + SymSearch = newSymbol(asciiString("Symbol.search")) + SymSpecies = newSymbol(asciiString("Symbol.species")) + SymSplit = newSymbol(asciiString("Symbol.split")) + SymToPrimitive = newSymbol(asciiString("Symbol.toPrimitive")) + SymToStringTag = newSymbol(asciiString("Symbol.toStringTag")) + SymUnscopables = newSymbol(asciiString("Symbol.unscopables")) +) + +func (r *Runtime) builtin_symbol(call FunctionCall) Value { + var desc String + if arg := call.Argument(0); !IsUndefined(arg) { + desc = arg.toString() + } + return newSymbol(desc) +} + +func (r *Runtime) symbolproto_tostring(call FunctionCall) Value { + sym, ok := call.This.(*Symbol) + if !ok { + if obj, ok := call.This.(*Object); ok { + if v, ok := obj.self.(*primitiveValueObject); ok { + if sym1, ok := v.pValue.(*Symbol); ok { + sym = sym1 + } + } + } + } + if sym == nil { + panic(r.NewTypeError("Method Symbol.prototype.toString is called on incompatible receiver")) + } + return sym.descriptiveString() +} + +func (r *Runtime) symbolproto_valueOf(call FunctionCall) Value { + _, ok := call.This.(*Symbol) + if ok { + return call.This + } + + if obj, ok := call.This.(*Object); ok { + if v, ok := obj.self.(*primitiveValueObject); ok { + if sym, ok := v.pValue.(*Symbol); ok { + return sym + } + } + } + + panic(r.NewTypeError("Symbol.prototype.valueOf requires that 'this' be a Symbol")) +} + +func (r *Runtime) symbol_for(call FunctionCall) Value { + key := call.Argument(0).toString() + keyStr := key.string() + if v := r.symbolRegistry[keyStr]; v != nil { + return v + } + if r.symbolRegistry == nil { + r.symbolRegistry = make(map[unistring.String]*Symbol) + } + v := newSymbol(key) + r.symbolRegistry[keyStr] = v + return v +} + +func (r *Runtime) symbol_keyfor(call FunctionCall) Value { + arg := call.Argument(0) + sym, ok := arg.(*Symbol) + if !ok { + panic(r.NewTypeError("%s is not a symbol", arg.String())) + } + for key, s := range r.symbolRegistry { + if s == sym { + return stringValueFromRaw(key) + } + } + return _undefined +} + +func (r *Runtime) thisSymbolValue(v Value) *Symbol { + if sym, ok := v.(*Symbol); ok { + return sym + } + if obj, ok := v.(*Object); ok { + if pVal, ok := obj.self.(*primitiveValueObject); ok { + if sym, ok := pVal.pValue.(*Symbol); ok { + return sym + } + } + } + panic(r.NewTypeError("Value is not a Symbol")) +} + +func (r *Runtime) createSymbolProto(val *Object) objectImpl { + o := &baseObject{ + class: classObject, + val: val, + extensible: true, + prototype: r.global.ObjectPrototype, + } + o.init() + + o._putProp("constructor", r.getSymbol(), true, false, true) + o.setOwnStr("description", &valueProperty{ + configurable: true, + getterFunc: r.newNativeFunc(func(call FunctionCall) Value { + return r.thisSymbolValue(call.This).desc + }, "get description", 0), + accessor: true, + }, false) + o._putProp("toString", r.newNativeFunc(r.symbolproto_tostring, "toString", 0), true, false, true) + o._putProp("valueOf", r.newNativeFunc(r.symbolproto_valueOf, "valueOf", 0), true, false, true) + o._putSym(SymToPrimitive, valueProp(r.newNativeFunc(r.symbolproto_valueOf, "[Symbol.toPrimitive]", 1), false, false, true)) + o._putSym(SymToStringTag, valueProp(newStringValue("Symbol"), false, false, true)) + + return o +} + +func (r *Runtime) createSymbol(val *Object) objectImpl { + o := r.newNativeFuncAndConstruct(val, r.builtin_symbol, func(args []Value, newTarget *Object) *Object { + panic(r.NewTypeError("Symbol is not a constructor")) + }, r.getSymbolPrototype(), "Symbol", _positiveZero) + + o._putProp("for", r.newNativeFunc(r.symbol_for, "for", 1), true, false, true) + o._putProp("keyFor", r.newNativeFunc(r.symbol_keyfor, "keyFor", 1), true, false, true) + + for _, s := range []*Symbol{ + SymHasInstance, + SymIsConcatSpreadable, + SymIterator, + SymMatch, + SymMatchAll, + SymReplace, + SymSearch, + SymSpecies, + SymSplit, + SymToPrimitive, + SymToStringTag, + SymUnscopables, + } { + n := s.desc.(asciiString) + n = n[len("Symbol."):] + o._putProp(unistring.String(n), s, false, false, false) + } + + return o +} + +func (r *Runtime) getSymbolPrototype() *Object { + ret := r.global.SymbolPrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.SymbolPrototype = ret + ret.self = r.createSymbolProto(ret) + } + return ret +} + +func (r *Runtime) getSymbol() *Object { + ret := r.global.Symbol + if ret == nil { + ret = &Object{runtime: r} + r.global.Symbol = ret + ret.self = r.createSymbol(ret) + } + return ret +} diff --git a/goja/builtin_typedarrays.go b/goja/builtin_typedarrays.go new file mode 100644 index 0000000..38c0376 --- /dev/null +++ b/goja/builtin_typedarrays.go @@ -0,0 +1,1973 @@ +package goja + +import ( + "fmt" + "math" + "sort" + "strings" + "sync" + "unsafe" + + "github.com/dop251/goja/unistring" +) + +type typedArraySortCtx struct { + ta *typedArrayObject + compare func(FunctionCall) Value + needValidate bool + detached bool +} + +func (ctx *typedArraySortCtx) Len() int { + return ctx.ta.length +} + +func (ctx *typedArraySortCtx) checkDetached() { + if !ctx.detached && ctx.needValidate { + ctx.detached = !ctx.ta.viewedArrayBuf.ensureNotDetached(false) + ctx.needValidate = false + } +} + +func (ctx *typedArraySortCtx) Less(i, j int) bool { + ctx.checkDetached() + if ctx.detached { + return false + } + offset := ctx.ta.offset + if ctx.compare != nil { + x := ctx.ta.typedArray.get(offset + i) + y := ctx.ta.typedArray.get(offset + j) + res := ctx.compare(FunctionCall{ + This: _undefined, + Arguments: []Value{x, y}, + }).ToNumber() + ctx.needValidate = true + if i, ok := res.(valueInt); ok { + return i < 0 + } + f := res.ToFloat() + if f < 0 { + return true + } + if f > 0 { + return false + } + if math.Signbit(f) { + return true + } + return false + } + + return ctx.ta.typedArray.less(offset+i, offset+j) +} + +func (ctx *typedArraySortCtx) Swap(i, j int) { + ctx.checkDetached() + if ctx.detached { + return + } + offset := ctx.ta.offset + ctx.ta.typedArray.swap(offset+i, offset+j) +} + +func allocByteSlice(size int) (b []byte) { + defer func() { + if x := recover(); x != nil { + panic(rangeError(fmt.Sprintf("Buffer size is too large: %d", size))) + } + }() + if size < 0 { + panic(rangeError(fmt.Sprintf("Invalid buffer size: %d", size))) + } + b = make([]byte, size) + return +} + +func (r *Runtime) builtin_newArrayBuffer(args []Value, newTarget *Object) *Object { + if newTarget == nil { + panic(r.needNew("ArrayBuffer")) + } + b := r._newArrayBuffer(r.getPrototypeFromCtor(newTarget, r.getArrayBuffer(), r.getArrayBufferPrototype()), nil) + if len(args) > 0 { + b.data = allocByteSlice(r.toIndex(args[0])) + } + return b.val +} + +func (r *Runtime) arrayBufferProto_getByteLength(call FunctionCall) Value { + o := r.toObject(call.This) + if b, ok := o.self.(*arrayBufferObject); ok { + if b.ensureNotDetached(false) { + return intToValue(int64(len(b.data))) + } + return intToValue(0) + } + panic(r.NewTypeError("Object is not ArrayBuffer: %s", o)) +} + +func (r *Runtime) arrayBufferProto_slice(call FunctionCall) Value { + o := r.toObject(call.This) + if b, ok := o.self.(*arrayBufferObject); ok { + l := int64(len(b.data)) + start := relToIdx(call.Argument(0).ToInteger(), l) + var stop int64 + if arg := call.Argument(1); arg != _undefined { + stop = arg.ToInteger() + } else { + stop = l + } + stop = relToIdx(stop, l) + newLen := max(stop-start, 0) + ret := r.speciesConstructor(o, r.getArrayBuffer())([]Value{intToValue(newLen)}, nil) + if ab, ok := ret.self.(*arrayBufferObject); ok { + if newLen > 0 { + b.ensureNotDetached(true) + if ret == o { + panic(r.NewTypeError("Species constructor returned the same ArrayBuffer")) + } + if int64(len(ab.data)) < newLen { + panic(r.NewTypeError("Species constructor returned an ArrayBuffer that is too small: %d", len(ab.data))) + } + ab.ensureNotDetached(true) + copy(ab.data, b.data[start:stop]) + } + return ret + } + panic(r.NewTypeError("Species constructor did not return an ArrayBuffer: %s", ret.String())) + } + panic(r.NewTypeError("Object is not ArrayBuffer: %s", o)) +} + +func (r *Runtime) arrayBuffer_isView(call FunctionCall) Value { + if o, ok := call.Argument(0).(*Object); ok { + if _, ok := o.self.(*dataViewObject); ok { + return valueTrue + } + if _, ok := o.self.(*typedArrayObject); ok { + return valueTrue + } + } + return valueFalse +} + +func (r *Runtime) newDataView(args []Value, newTarget *Object) *Object { + if newTarget == nil { + panic(r.needNew("DataView")) + } + var bufArg Value + if len(args) > 0 { + bufArg = args[0] + } + var buffer *arrayBufferObject + if o, ok := bufArg.(*Object); ok { + if b, ok := o.self.(*arrayBufferObject); ok { + buffer = b + } + } + if buffer == nil { + panic(r.NewTypeError("First argument to DataView constructor must be an ArrayBuffer")) + } + var byteOffset, byteLen int + if len(args) > 1 { + offsetArg := nilSafe(args[1]) + byteOffset = r.toIndex(offsetArg) + buffer.ensureNotDetached(true) + if byteOffset > len(buffer.data) { + panic(r.newError(r.getRangeError(), "Start offset %s is outside the bounds of the buffer", offsetArg.String())) + } + } + if len(args) > 2 && args[2] != nil && args[2] != _undefined { + byteLen = r.toIndex(args[2]) + if byteOffset+byteLen > len(buffer.data) { + panic(r.newError(r.getRangeError(), "Invalid DataView length %d", byteLen)) + } + } else { + byteLen = len(buffer.data) - byteOffset + } + proto := r.getPrototypeFromCtor(newTarget, r.getDataView(), r.getDataViewPrototype()) + buffer.ensureNotDetached(true) + if byteOffset > len(buffer.data) { + panic(r.newError(r.getRangeError(), "Start offset %d is outside the bounds of the buffer", byteOffset)) + } + if byteOffset+byteLen > len(buffer.data) { + panic(r.newError(r.getRangeError(), "Invalid DataView length %d", byteLen)) + } + o := &Object{runtime: r} + b := &dataViewObject{ + baseObject: baseObject{ + class: classObject, + val: o, + prototype: proto, + extensible: true, + }, + viewedArrayBuf: buffer, + byteOffset: byteOffset, + byteLen: byteLen, + } + o.self = b + b.init() + return o +} + +func (r *Runtime) dataViewProto_getBuffer(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + return dv.viewedArrayBuf.val + } + panic(r.NewTypeError("Method get DataView.prototype.buffer called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_getByteLen(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + dv.viewedArrayBuf.ensureNotDetached(true) + return intToValue(int64(dv.byteLen)) + } + panic(r.NewTypeError("Method get DataView.prototype.byteLength called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_getByteOffset(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + dv.viewedArrayBuf.ensureNotDetached(true) + return intToValue(int64(dv.byteOffset)) + } + panic(r.NewTypeError("Method get DataView.prototype.byteOffset called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_getFloat32(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + return floatToValue(float64(dv.viewedArrayBuf.getFloat32(dv.getIdxAndByteOrder(r.toIndex(call.Argument(0)), call.Argument(1), 4)))) + } + panic(r.NewTypeError("Method DataView.prototype.getFloat32 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_getFloat64(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + return floatToValue(dv.viewedArrayBuf.getFloat64(dv.getIdxAndByteOrder(r.toIndex(call.Argument(0)), call.Argument(1), 8))) + } + panic(r.NewTypeError("Method DataView.prototype.getFloat64 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_getInt8(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + idx, _ := dv.getIdxAndByteOrder(r.toIndex(call.Argument(0)), call.Argument(1), 1) + return intToValue(int64(dv.viewedArrayBuf.getInt8(idx))) + } + panic(r.NewTypeError("Method DataView.prototype.getInt8 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_getInt16(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + return intToValue(int64(dv.viewedArrayBuf.getInt16(dv.getIdxAndByteOrder(r.toIndex(call.Argument(0)), call.Argument(1), 2)))) + } + panic(r.NewTypeError("Method DataView.prototype.getInt16 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_getInt32(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + return intToValue(int64(dv.viewedArrayBuf.getInt32(dv.getIdxAndByteOrder(r.toIndex(call.Argument(0)), call.Argument(1), 4)))) + } + panic(r.NewTypeError("Method DataView.prototype.getInt32 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_getUint8(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + idx, _ := dv.getIdxAndByteOrder(r.toIndex(call.Argument(0)), call.Argument(1), 1) + return intToValue(int64(dv.viewedArrayBuf.getUint8(idx))) + } + panic(r.NewTypeError("Method DataView.prototype.getUint8 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_getUint16(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + return intToValue(int64(dv.viewedArrayBuf.getUint16(dv.getIdxAndByteOrder(r.toIndex(call.Argument(0)), call.Argument(1), 2)))) + } + panic(r.NewTypeError("Method DataView.prototype.getUint16 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_getUint32(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + return intToValue(int64(dv.viewedArrayBuf.getUint32(dv.getIdxAndByteOrder(r.toIndex(call.Argument(0)), call.Argument(1), 4)))) + } + panic(r.NewTypeError("Method DataView.prototype.getUint32 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_getBigInt64(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + return (*valueBigInt)(dv.viewedArrayBuf.getBigInt64(dv.getIdxAndByteOrder(r.toIndex(call.Argument(0).ToNumber()), call.Argument(1), 8))) + } + panic(r.NewTypeError("Method DataView.prototype.getBigInt64 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_getBigUint64(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + return (*valueBigInt)(dv.viewedArrayBuf.getBigUint64(dv.getIdxAndByteOrder(r.toIndex(call.Argument(0).ToNumber()), call.Argument(1), 8))) + } + panic(r.NewTypeError("Method DataView.prototype.getBigUint64 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_setFloat32(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + idxVal := r.toIndex(call.Argument(0)) + val := toFloat32(call.Argument(1)) + idx, bo := dv.getIdxAndByteOrder(idxVal, call.Argument(2), 4) + dv.viewedArrayBuf.setFloat32(idx, val, bo) + return _undefined + } + panic(r.NewTypeError("Method DataView.prototype.setFloat32 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_setFloat64(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + idxVal := r.toIndex(call.Argument(0)) + val := call.Argument(1).ToFloat() + idx, bo := dv.getIdxAndByteOrder(idxVal, call.Argument(2), 8) + dv.viewedArrayBuf.setFloat64(idx, val, bo) + return _undefined + } + panic(r.NewTypeError("Method DataView.prototype.setFloat64 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_setInt8(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + idxVal := r.toIndex(call.Argument(0)) + val := toInt8(call.Argument(1)) + idx, _ := dv.getIdxAndByteOrder(idxVal, call.Argument(2), 1) + dv.viewedArrayBuf.setInt8(idx, val) + return _undefined + } + panic(r.NewTypeError("Method DataView.prototype.setInt8 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_setInt16(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + idxVal := r.toIndex(call.Argument(0)) + val := toInt16(call.Argument(1)) + idx, bo := dv.getIdxAndByteOrder(idxVal, call.Argument(2), 2) + dv.viewedArrayBuf.setInt16(idx, val, bo) + return _undefined + } + panic(r.NewTypeError("Method DataView.prototype.setInt16 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_setInt32(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + idxVal := r.toIndex(call.Argument(0)) + val := toInt32(call.Argument(1)) + idx, bo := dv.getIdxAndByteOrder(idxVal, call.Argument(2), 4) + dv.viewedArrayBuf.setInt32(idx, val, bo) + return _undefined + } + panic(r.NewTypeError("Method DataView.prototype.setInt32 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_setUint8(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + idxVal := r.toIndex(call.Argument(0)) + val := toUint8(call.Argument(1)) + idx, _ := dv.getIdxAndByteOrder(idxVal, call.Argument(2), 1) + dv.viewedArrayBuf.setUint8(idx, val) + return _undefined + } + panic(r.NewTypeError("Method DataView.prototype.setUint8 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_setUint16(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + idxVal := r.toIndex(call.Argument(0)) + val := toUint16(call.Argument(1)) + idx, bo := dv.getIdxAndByteOrder(idxVal, call.Argument(2), 2) + dv.viewedArrayBuf.setUint16(idx, val, bo) + return _undefined + } + panic(r.NewTypeError("Method DataView.prototype.setUint16 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_setUint32(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + idxVal := r.toIndex(call.Argument(0)) + val := toUint32(call.Argument(1)) + idx, bo := dv.getIdxAndByteOrder(idxVal, call.Argument(2), 4) + dv.viewedArrayBuf.setUint32(idx, val, bo) + return _undefined + } + panic(r.NewTypeError("Method DataView.prototype.setUint32 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_setBigInt64(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + idxVal := r.toIndex(call.Argument(0)) + val := toBigInt64(call.Argument(1)) + idx, bo := dv.getIdxAndByteOrder(idxVal, call.Argument(2), 8) + dv.viewedArrayBuf.setBigInt64(idx, val, bo) + return _undefined + } + panic(r.NewTypeError("Method DataView.prototype.setBigInt64 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) dataViewProto_setBigUint64(call FunctionCall) Value { + if dv, ok := r.toObject(call.This).self.(*dataViewObject); ok { + idxVal := r.toIndex(call.Argument(0)) + val := toBigUint64(call.Argument(1)) + idx, bo := dv.getIdxAndByteOrder(idxVal, call.Argument(2), 8) + dv.viewedArrayBuf.setBigUint64(idx, val, bo) + return _undefined + } + panic(r.NewTypeError("Method DataView.prototype.setBigUint64 called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_getBuffer(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + return ta.viewedArrayBuf.val + } + panic(r.NewTypeError("Method get TypedArray.prototype.buffer called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_getByteLen(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + if ta.viewedArrayBuf.data == nil { + return _positiveZero + } + return intToValue(int64(ta.length) * int64(ta.elemSize)) + } + panic(r.NewTypeError("Method get TypedArray.prototype.byteLength called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_getLength(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + if ta.viewedArrayBuf.data == nil { + return _positiveZero + } + return intToValue(int64(ta.length)) + } + panic(r.NewTypeError("Method get TypedArray.prototype.length called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_getByteOffset(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + if ta.viewedArrayBuf.data == nil { + return _positiveZero + } + return intToValue(int64(ta.offset) * int64(ta.elemSize)) + } + panic(r.NewTypeError("Method get TypedArray.prototype.byteOffset called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_copyWithin(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + l := int64(ta.length) + var relEnd int64 + to := toIntStrict(relToIdx(call.Argument(0).ToInteger(), l)) + from := toIntStrict(relToIdx(call.Argument(1).ToInteger(), l)) + if end := call.Argument(2); end != _undefined { + relEnd = end.ToInteger() + } else { + relEnd = l + } + final := toIntStrict(relToIdx(relEnd, l)) + data := ta.viewedArrayBuf.data + offset := ta.offset + elemSize := ta.elemSize + if final > from { + ta.viewedArrayBuf.ensureNotDetached(true) + copy(data[(offset+to)*elemSize:], data[(offset+from)*elemSize:(offset+final)*elemSize]) + } + return call.This + } + panic(r.NewTypeError("Method TypedArray.prototype.copyWithin called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_entries(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + return r.createArrayIterator(ta.val, iterationKindKeyValue) + } + panic(r.NewTypeError("Method TypedArray.prototype.entries called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_every(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, call.This}, + } + for k := 0; k < ta.length; k++ { + if ta.isValidIntegerIndex(k) { + fc.Arguments[0] = ta.typedArray.get(ta.offset + k) + } else { + fc.Arguments[0] = _undefined + } + fc.Arguments[1] = intToValue(int64(k)) + if !callbackFn(fc).ToBoolean() { + return valueFalse + } + } + return valueTrue + + } + panic(r.NewTypeError("Method TypedArray.prototype.every called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_fill(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + l := int64(ta.length) + k := toIntStrict(relToIdx(call.Argument(1).ToInteger(), l)) + var relEnd int64 + if endArg := call.Argument(2); endArg != _undefined { + relEnd = endArg.ToInteger() + } else { + relEnd = l + } + final := toIntStrict(relToIdx(relEnd, l)) + value := ta.typedArray.toRaw(call.Argument(0)) + ta.viewedArrayBuf.ensureNotDetached(true) + for ; k < final; k++ { + ta.typedArray.setRaw(ta.offset+k, value) + } + return call.This + } + panic(r.NewTypeError("Method TypedArray.prototype.fill called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_filter(call FunctionCall) Value { + o := r.toObject(call.This) + if ta, ok := o.self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, call.This}, + } + buf := make([]byte, 0, ta.length*ta.elemSize) + captured := 0 + rawVal := make([]byte, ta.elemSize) + for k := 0; k < ta.length; k++ { + if ta.isValidIntegerIndex(k) { + fc.Arguments[0] = ta.typedArray.get(ta.offset + k) + i := (ta.offset + k) * ta.elemSize + copy(rawVal, ta.viewedArrayBuf.data[i:]) + } else { + fc.Arguments[0] = _undefined + for i := range rawVal { + rawVal[i] = 0 + } + } + fc.Arguments[1] = intToValue(int64(k)) + if callbackFn(fc).ToBoolean() { + buf = append(buf, rawVal...) + captured++ + } + } + c := r.speciesConstructorObj(o, ta.defaultCtor) + ab := r._newArrayBuffer(r.getArrayBufferPrototype(), nil) + ab.data = buf + kept := r.toConstructor(ta.defaultCtor)([]Value{ab.val}, ta.defaultCtor) + if c == ta.defaultCtor { + return kept + } else { + ret := r.typedArrayCreate(c, intToValue(int64(captured))) + keptTa := kept.self.(*typedArrayObject) + for i := 0; i < captured; i++ { + ret.typedArray.set(i, keptTa.typedArray.get(keptTa.offset+i)) + } + return ret.val + } + } + panic(r.NewTypeError("Method TypedArray.prototype.filter called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_find(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + predicate := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, call.This}, + } + for k := 0; k < ta.length; k++ { + var val Value + if ta.isValidIntegerIndex(k) { + val = ta.typedArray.get(ta.offset + k) + } + fc.Arguments[0] = val + fc.Arguments[1] = intToValue(int64(k)) + if predicate(fc).ToBoolean() { + return val + } + } + return _undefined + } + panic(r.NewTypeError("Method TypedArray.prototype.find called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_findIndex(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + predicate := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, call.This}, + } + for k := 0; k < ta.length; k++ { + if ta.isValidIntegerIndex(k) { + fc.Arguments[0] = ta.typedArray.get(ta.offset + k) + } else { + fc.Arguments[0] = _undefined + } + fc.Arguments[1] = intToValue(int64(k)) + if predicate(fc).ToBoolean() { + return fc.Arguments[1] + } + } + return intToValue(-1) + } + panic(r.NewTypeError("Method TypedArray.prototype.findIndex called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_findLast(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + predicate := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, call.This}, + } + for k := ta.length - 1; k >= 0; k-- { + var val Value + if ta.isValidIntegerIndex(k) { + val = ta.typedArray.get(ta.offset + k) + } + fc.Arguments[0] = val + fc.Arguments[1] = intToValue(int64(k)) + if predicate(fc).ToBoolean() { + return val + } + } + return _undefined + } + panic(r.NewTypeError("Method TypedArray.prototype.findLast called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_findLastIndex(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + predicate := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, call.This}, + } + for k := ta.length - 1; k >= 0; k-- { + if ta.isValidIntegerIndex(k) { + fc.Arguments[0] = ta.typedArray.get(ta.offset + k) + } else { + fc.Arguments[0] = _undefined + } + fc.Arguments[1] = intToValue(int64(k)) + if predicate(fc).ToBoolean() { + return fc.Arguments[1] + } + } + return intToValue(-1) + } + panic(r.NewTypeError("Method TypedArray.prototype.findLastIndex called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_forEach(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, call.This}, + } + for k := 0; k < ta.length; k++ { + var val Value + if ta.isValidIntegerIndex(k) { + val = ta.typedArray.get(ta.offset + k) + } + fc.Arguments[0] = val + fc.Arguments[1] = intToValue(int64(k)) + callbackFn(fc) + } + return _undefined + } + panic(r.NewTypeError("Method TypedArray.prototype.forEach called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_includes(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + length := int64(ta.length) + if length == 0 { + return valueFalse + } + + n := call.Argument(1).ToInteger() + if n >= length { + return valueFalse + } + + if n < 0 { + n = max(length+n, 0) + } + + searchElement := call.Argument(0) + if searchElement == _negativeZero { + searchElement = _positiveZero + } + startIdx := toIntStrict(n) + if !ta.viewedArrayBuf.ensureNotDetached(false) { + if searchElement == _undefined && startIdx < ta.length { + return valueTrue + } + return valueFalse + } + if ta.typedArray.typeMatch(searchElement) { + se := ta.typedArray.toRaw(searchElement) + for k := startIdx; k < ta.length; k++ { + if ta.typedArray.getRaw(ta.offset+k) == se { + return valueTrue + } + } + } + return valueFalse + } + panic(r.NewTypeError("Method TypedArray.prototype.includes called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_at(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + idx := call.Argument(0).ToInteger() + length := int64(ta.length) + if idx < 0 { + idx = length + idx + } + if idx >= length || idx < 0 { + return _undefined + } + if ta.viewedArrayBuf.ensureNotDetached(false) { + return ta.typedArray.get(ta.offset + int(idx)) + } + return _undefined + } + panic(r.NewTypeError("Method TypedArray.prototype.at called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_indexOf(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + length := int64(ta.length) + if length == 0 { + return intToValue(-1) + } + + n := call.Argument(1).ToInteger() + if n >= length { + return intToValue(-1) + } + + if n < 0 { + n = max(length+n, 0) + } + + if ta.viewedArrayBuf.ensureNotDetached(false) { + searchElement := call.Argument(0) + if searchElement == _negativeZero { + searchElement = _positiveZero + } + if !IsNaN(searchElement) && ta.typedArray.typeMatch(searchElement) { + se := ta.typedArray.toRaw(searchElement) + for k := toIntStrict(n); k < ta.length; k++ { + if ta.typedArray.getRaw(ta.offset+k) == se { + return intToValue(int64(k)) + } + } + } + } + return intToValue(-1) + } + panic(r.NewTypeError("Method TypedArray.prototype.indexOf called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_join(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + s := call.Argument(0) + var sep String + if s != _undefined { + sep = s.toString() + } else { + sep = asciiString(",") + } + l := ta.length + if l == 0 { + return stringEmpty + } + + var buf StringBuilder + + var element0 Value + if ta.isValidIntegerIndex(0) { + element0 = ta.typedArray.get(ta.offset + 0) + } + if element0 != nil && element0 != _undefined && element0 != _null { + buf.WriteString(element0.toString()) + } + + for i := 1; i < l; i++ { + buf.WriteString(sep) + if ta.isValidIntegerIndex(i) { + element := ta.typedArray.get(ta.offset + i) + if element != nil && element != _undefined && element != _null { + buf.WriteString(element.toString()) + } + } + } + + return buf.String() + } + panic(r.NewTypeError("Method TypedArray.prototype.join called on incompatible receiver")) +} + +func (r *Runtime) typedArrayProto_keys(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + return r.createArrayIterator(ta.val, iterationKindKey) + } + panic(r.NewTypeError("Method TypedArray.prototype.keys called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_lastIndexOf(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + length := int64(ta.length) + if length == 0 { + return intToValue(-1) + } + + var fromIndex int64 + + if len(call.Arguments) < 2 { + fromIndex = length - 1 + } else { + fromIndex = call.Argument(1).ToInteger() + if fromIndex >= 0 { + fromIndex = min(fromIndex, length-1) + } else { + fromIndex += length + if fromIndex < 0 { + fromIndex = -1 // prevent underflow in toIntStrict() on 32-bit platforms + } + } + } + + if ta.viewedArrayBuf.ensureNotDetached(false) { + searchElement := call.Argument(0) + if searchElement == _negativeZero { + searchElement = _positiveZero + } + if !IsNaN(searchElement) && ta.typedArray.typeMatch(searchElement) { + se := ta.typedArray.toRaw(searchElement) + for k := toIntStrict(fromIndex); k >= 0; k-- { + if ta.typedArray.getRaw(ta.offset+k) == se { + return intToValue(int64(k)) + } + } + } + } + + return intToValue(-1) + } + panic(r.NewTypeError("Method TypedArray.prototype.lastIndexOf called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_map(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, call.This}, + } + dst := r.typedArraySpeciesCreate(ta, []Value{intToValue(int64(ta.length))}) + for i := 0; i < ta.length; i++ { + if ta.isValidIntegerIndex(i) { + fc.Arguments[0] = ta.typedArray.get(ta.offset + i) + } else { + fc.Arguments[0] = _undefined + } + fc.Arguments[1] = intToValue(int64(i)) + dst.typedArray.set(i, callbackFn(fc)) + } + return dst.val + } + panic(r.NewTypeError("Method TypedArray.prototype.map called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_reduce(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: _undefined, + Arguments: []Value{nil, nil, nil, call.This}, + } + k := 0 + if len(call.Arguments) >= 2 { + fc.Arguments[0] = call.Argument(1) + } else { + if ta.length > 0 { + fc.Arguments[0] = ta.typedArray.get(ta.offset + 0) + k = 1 + } + } + if fc.Arguments[0] == nil { + panic(r.NewTypeError("Reduce of empty array with no initial value")) + } + for ; k < ta.length; k++ { + if ta.isValidIntegerIndex(k) { + fc.Arguments[1] = ta.typedArray.get(ta.offset + k) + } else { + fc.Arguments[1] = _undefined + } + idx := valueInt(k) + fc.Arguments[2] = idx + fc.Arguments[0] = callbackFn(fc) + } + return fc.Arguments[0] + } + panic(r.NewTypeError("Method TypedArray.prototype.reduce called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_reduceRight(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: _undefined, + Arguments: []Value{nil, nil, nil, call.This}, + } + k := ta.length - 1 + if len(call.Arguments) >= 2 { + fc.Arguments[0] = call.Argument(1) + } else { + if k >= 0 { + fc.Arguments[0] = ta.typedArray.get(ta.offset + k) + k-- + } + } + if fc.Arguments[0] == nil { + panic(r.NewTypeError("Reduce of empty array with no initial value")) + } + for ; k >= 0; k-- { + if ta.isValidIntegerIndex(k) { + fc.Arguments[1] = ta.typedArray.get(ta.offset + k) + } else { + fc.Arguments[1] = _undefined + } + idx := valueInt(k) + fc.Arguments[2] = idx + fc.Arguments[0] = callbackFn(fc) + } + return fc.Arguments[0] + } + panic(r.NewTypeError("Method TypedArray.prototype.reduceRight called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_reverse(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + l := ta.length + middle := l / 2 + for lower := 0; lower != middle; lower++ { + upper := l - lower - 1 + ta.typedArray.swap(ta.offset+lower, ta.offset+upper) + } + + return call.This + } + panic(r.NewTypeError("Method TypedArray.prototype.reverse called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_set(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + srcObj := call.Argument(0).ToObject(r) + targetOffset := toIntStrict(call.Argument(1).ToInteger()) + if targetOffset < 0 { + panic(r.newError(r.getRangeError(), "offset should be >= 0")) + } + ta.viewedArrayBuf.ensureNotDetached(true) + targetLen := ta.length + if src, ok := srcObj.self.(*typedArrayObject); ok { + src.viewedArrayBuf.ensureNotDetached(true) + srcLen := src.length + if x := srcLen + targetOffset; x < 0 || x > targetLen { + panic(r.newError(r.getRangeError(), "Source is too large")) + } + if src.defaultCtor == ta.defaultCtor { + copy(ta.viewedArrayBuf.data[(ta.offset+targetOffset)*ta.elemSize:], + src.viewedArrayBuf.data[src.offset*src.elemSize:(src.offset+srcLen)*src.elemSize]) + } else { + checkTypedArrayMixBigInt(src.defaultCtor, ta.defaultCtor) + curSrc := uintptr(unsafe.Pointer(&src.viewedArrayBuf.data[src.offset*src.elemSize])) + endSrc := curSrc + uintptr(srcLen*src.elemSize) + curDst := uintptr(unsafe.Pointer(&ta.viewedArrayBuf.data[(ta.offset+targetOffset)*ta.elemSize])) + dstOffset := ta.offset + targetOffset + srcOffset := src.offset + if ta.elemSize == src.elemSize { + if curDst <= curSrc || curDst >= endSrc { + for i := 0; i < srcLen; i++ { + ta.typedArray.set(dstOffset+i, src.typedArray.get(srcOffset+i)) + } + } else { + for i := srcLen - 1; i >= 0; i-- { + ta.typedArray.set(dstOffset+i, src.typedArray.get(srcOffset+i)) + } + } + } else { + x := int(curDst-curSrc) / (src.elemSize - ta.elemSize) + if x < 0 { + x = 0 + } else if x > srcLen { + x = srcLen + } + if ta.elemSize < src.elemSize { + for i := x; i < srcLen; i++ { + ta.typedArray.set(dstOffset+i, src.typedArray.get(srcOffset+i)) + } + for i := x - 1; i >= 0; i-- { + ta.typedArray.set(dstOffset+i, src.typedArray.get(srcOffset+i)) + } + } else { + for i := 0; i < x; i++ { + ta.typedArray.set(dstOffset+i, src.typedArray.get(srcOffset+i)) + } + for i := srcLen - 1; i >= x; i-- { + ta.typedArray.set(dstOffset+i, src.typedArray.get(srcOffset+i)) + } + } + } + } + } else { + targetLen := ta.length + srcLen := toIntStrict(toLength(srcObj.self.getStr("length", nil))) + if x := srcLen + targetOffset; x < 0 || x > targetLen { + panic(r.newError(r.getRangeError(), "Source is too large")) + } + for i := 0; i < srcLen; i++ { + val := nilSafe(srcObj.self.getIdx(valueInt(i), nil)) + if ta.isValidIntegerIndex(i) { + ta.typedArray.set(targetOffset+i, val) + } + } + } + return _undefined + } + panic(r.NewTypeError("Method TypedArray.prototype.set called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_slice(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + length := int64(ta.length) + start := toIntStrict(relToIdx(call.Argument(0).ToInteger(), length)) + var e int64 + if endArg := call.Argument(1); endArg != _undefined { + e = endArg.ToInteger() + } else { + e = length + } + end := toIntStrict(relToIdx(e, length)) + + count := end - start + if count < 0 { + count = 0 + } + dst := r.typedArraySpeciesCreate(ta, []Value{intToValue(int64(count))}) + if dst.defaultCtor == ta.defaultCtor { + if count > 0 { + ta.viewedArrayBuf.ensureNotDetached(true) + offset := ta.offset + elemSize := ta.elemSize + copy(dst.viewedArrayBuf.data, ta.viewedArrayBuf.data[(offset+start)*elemSize:(offset+start+count)*elemSize]) + } + } else { + for i := 0; i < count; i++ { + ta.viewedArrayBuf.ensureNotDetached(true) + dst.typedArray.set(i, ta.typedArray.get(ta.offset+start+i)) + } + } + return dst.val + } + panic(r.NewTypeError("Method TypedArray.prototype.slice called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_some(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + callbackFn := r.toCallable(call.Argument(0)) + fc := FunctionCall{ + This: call.Argument(1), + Arguments: []Value{nil, nil, call.This}, + } + for k := 0; k < ta.length; k++ { + if ta.isValidIntegerIndex(k) { + fc.Arguments[0] = ta.typedArray.get(ta.offset + k) + } else { + fc.Arguments[0] = _undefined + } + fc.Arguments[1] = intToValue(int64(k)) + if callbackFn(fc).ToBoolean() { + return valueTrue + } + } + return valueFalse + } + panic(r.NewTypeError("Method TypedArray.prototype.some called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_sort(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + var compareFn func(FunctionCall) Value + + if arg := call.Argument(0); arg != _undefined { + compareFn = r.toCallable(arg) + } + + ctx := typedArraySortCtx{ + ta: ta, + compare: compareFn, + } + + sort.Stable(&ctx) + return call.This + } + panic(r.NewTypeError("Method TypedArray.prototype.sort called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_subarray(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + l := int64(ta.length) + beginIdx := relToIdx(call.Argument(0).ToInteger(), l) + var relEnd int64 + if endArg := call.Argument(1); endArg != _undefined { + relEnd = endArg.ToInteger() + } else { + relEnd = l + } + endIdx := relToIdx(relEnd, l) + newLen := max(endIdx-beginIdx, 0) + return r.typedArraySpeciesCreate(ta, []Value{ta.viewedArrayBuf.val, + intToValue((int64(ta.offset) + beginIdx) * int64(ta.elemSize)), + intToValue(newLen), + }).val + } + panic(r.NewTypeError("Method TypedArray.prototype.subarray called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_toLocaleString(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + length := ta.length + var buf StringBuilder + for i := 0; i < length; i++ { + ta.viewedArrayBuf.ensureNotDetached(true) + if i > 0 { + buf.WriteRune(',') + } + item := ta.typedArray.get(ta.offset + i) + r.writeItemLocaleString(item, &buf) + } + return buf.String() + } + panic(r.NewTypeError("Method TypedArray.prototype.toLocaleString called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_values(call FunctionCall) Value { + if ta, ok := r.toObject(call.This).self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + return r.createArrayIterator(ta.val, iterationKindValue) + } + panic(r.NewTypeError("Method TypedArray.prototype.values called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: call.This}))) +} + +func (r *Runtime) typedArrayProto_toStringTag(call FunctionCall) Value { + if obj, ok := call.This.(*Object); ok { + if ta, ok := obj.self.(*typedArrayObject); ok { + return nilSafe(ta.defaultCtor.self.getStr("name", nil)) + } + } + + return _undefined +} + +func (r *Runtime) typedArrayProto_with(call FunctionCall) Value { + o := call.This.ToObject(r) + ta, ok := o.self.(*typedArrayObject) + if !ok { + panic(r.NewTypeError("%s is not a valid TypedArray", r.objectproto_toString(FunctionCall{This: call.This}))) + } + ta.viewedArrayBuf.ensureNotDetached(true) + length := ta.length + relativeIndex := call.Argument(0).ToInteger() + var actualIndex int + + if relativeIndex >= 0 { + actualIndex = toIntStrict(relativeIndex) + } else { + actualIndex = toIntStrict(int64(length) + relativeIndex) + } + if !ta.isValidIntegerIndex(actualIndex) { + panic(r.newError(r.getRangeError(), "Invalid typed array index")) + } + + var numericValue Value + switch ta.typedArray.(type) { + case *bigInt64Array, *bigUint64Array: + numericValue = toBigInt(call.Argument(1)) + default: + numericValue = call.Argument(1).ToNumber() + } + + a := r.typedArrayCreate(ta.defaultCtor, intToValue(int64(length))) + for k := 0; k < length; k++ { + var fromValue Value + if k == actualIndex { + fromValue = numericValue + } else { + fromValue = ta.typedArray.get(ta.offset + k) + } + a.typedArray.set(ta.offset+k, fromValue) + } + return a.val +} + +func (r *Runtime) typedArrayProto_toReversed(call FunctionCall) Value { + o := call.This.ToObject(r) + ta, ok := o.self.(*typedArrayObject) + if !ok { + panic(r.NewTypeError("%s is not a valid TypedArray", r.objectproto_toString(FunctionCall{This: call.This}))) + } + ta.viewedArrayBuf.ensureNotDetached(true) + length := ta.length + + a := r.typedArrayCreate(ta.defaultCtor, intToValue(int64(length))) + + for k := 0; k < length; k++ { + from := length - k - 1 + fromValue := ta.typedArray.get(ta.offset + from) + a.typedArray.set(ta.offset+k, fromValue) + } + + return a.val +} + +func (r *Runtime) typedArrayProto_toSorted(call FunctionCall) Value { + o := call.This.ToObject(r) + ta, ok := o.self.(*typedArrayObject) + if !ok { + panic(r.NewTypeError("%s is not a valid TypedArray", r.objectproto_toString(FunctionCall{This: call.This}))) + } + ta.viewedArrayBuf.ensureNotDetached(true) + + var compareFn func(FunctionCall) Value + arg := call.Argument(0) + if arg != _undefined { + if arg, ok := arg.(*Object); ok { + compareFn, _ = arg.self.assertCallable() + } + if compareFn == nil { + panic(r.NewTypeError("The comparison function must be either a function or undefined")) + } + } + + length := ta.length + + a := r.typedArrayCreate(ta.defaultCtor, intToValue(int64(length))) + copy(a.viewedArrayBuf.data, ta.viewedArrayBuf.data) + + ctx := typedArraySortCtx{ + ta: a, + compare: compareFn, + } + + sort.Stable(&ctx) + + return a.val +} + +func (r *Runtime) newTypedArray([]Value, *Object) *Object { + panic(r.NewTypeError("Abstract class TypedArray not directly constructable")) +} + +func (r *Runtime) typedArray_from(call FunctionCall) Value { + c := r.toObject(call.This) + var mapFc func(call FunctionCall) Value + thisValue := call.Argument(2) + if mapFn := call.Argument(1); mapFn != _undefined { + mapFc = r.toCallable(mapFn) + } + source := r.toObject(call.Argument(0)) + usingIter := toMethod(source.self.getSym(SymIterator, nil)) + if usingIter != nil { + values := r.iterableToList(source, usingIter) + ta := r.typedArrayCreate(c, intToValue(int64(len(values)))) + if mapFc == nil { + for idx, val := range values { + ta.typedArray.set(idx, val) + } + } else { + fc := FunctionCall{ + This: thisValue, + Arguments: []Value{nil, nil}, + } + for idx, val := range values { + fc.Arguments[0], fc.Arguments[1] = val, intToValue(int64(idx)) + val = mapFc(fc) + ta._putIdx(idx, val) + } + } + return ta.val + } + length := toIntStrict(toLength(source.self.getStr("length", nil))) + ta := r.typedArrayCreate(c, intToValue(int64(length))) + if mapFc == nil { + for i := 0; i < length; i++ { + ta.typedArray.set(i, nilSafe(source.self.getIdx(valueInt(i), nil))) + } + } else { + fc := FunctionCall{ + This: thisValue, + Arguments: []Value{nil, nil}, + } + for i := 0; i < length; i++ { + idx := valueInt(i) + fc.Arguments[0], fc.Arguments[1] = source.self.getIdx(idx, nil), idx + ta.typedArray.set(i, mapFc(fc)) + } + } + return ta.val +} + +func (r *Runtime) typedArray_of(call FunctionCall) Value { + ta := r.typedArrayCreate(r.toObject(call.This), intToValue(int64(len(call.Arguments)))) + for i, val := range call.Arguments { + ta.typedArray.set(i, val) + } + return ta.val +} + +func (r *Runtime) allocateTypedArray(newTarget *Object, length int, taCtor typedArrayObjectCtor, proto *Object) *typedArrayObject { + buf := r._newArrayBuffer(r.getArrayBufferPrototype(), nil) + ta := taCtor(buf, 0, length, r.getPrototypeFromCtor(newTarget, nil, proto)) + if length > 0 { + buf.data = allocByteSlice(length * ta.elemSize) + } + return ta +} + +func (r *Runtime) typedArraySpeciesCreate(ta *typedArrayObject, args []Value) *typedArrayObject { + return r.typedArrayCreate(r.speciesConstructorObj(ta.val, ta.defaultCtor), args...) +} + +func (r *Runtime) typedArrayCreate(ctor *Object, args ...Value) *typedArrayObject { + o := r.toConstructor(ctor)(args, ctor) + if ta, ok := o.self.(*typedArrayObject); ok { + ta.viewedArrayBuf.ensureNotDetached(true) + if len(args) == 1 { + if l, ok := args[0].(valueInt); ok { + if ta.length < int(l) { + panic(r.NewTypeError("Derived TypedArray constructor created an array which was too small")) + } + } + } + return ta + } + panic(r.NewTypeError("Invalid TypedArray: %s", o)) +} + +func (r *Runtime) typedArrayFrom(ctor, items *Object, mapFn, thisValue Value, taCtor typedArrayObjectCtor, proto *Object) *Object { + var mapFc func(call FunctionCall) Value + if mapFn != nil { + mapFc = r.toCallable(mapFn) + if thisValue == nil { + thisValue = _undefined + } + } + usingIter := toMethod(items.self.getSym(SymIterator, nil)) + if usingIter != nil { + values := r.iterableToList(items, usingIter) + ta := r.allocateTypedArray(ctor, len(values), taCtor, proto) + if mapFc == nil { + for idx, val := range values { + ta.typedArray.set(idx, val) + } + } else { + fc := FunctionCall{ + This: thisValue, + Arguments: []Value{nil, nil}, + } + for idx, val := range values { + fc.Arguments[0], fc.Arguments[1] = val, intToValue(int64(idx)) + val = mapFc(fc) + ta.typedArray.set(idx, val) + } + } + return ta.val + } + length := toIntStrict(toLength(items.self.getStr("length", nil))) + ta := r.allocateTypedArray(ctor, length, taCtor, proto) + if mapFc == nil { + for i := 0; i < length; i++ { + ta.typedArray.set(i, nilSafe(items.self.getIdx(valueInt(i), nil))) + } + } else { + fc := FunctionCall{ + This: thisValue, + Arguments: []Value{nil, nil}, + } + for i := 0; i < length; i++ { + idx := valueInt(i) + fc.Arguments[0], fc.Arguments[1] = items.self.getIdx(idx, nil), idx + ta.typedArray.set(i, mapFc(fc)) + } + } + return ta.val +} + +func (r *Runtime) _newTypedArrayFromArrayBuffer(ab *arrayBufferObject, args []Value, newTarget *Object, taCtor typedArrayObjectCtor, proto *Object) *Object { + ta := taCtor(ab, 0, 0, r.getPrototypeFromCtor(newTarget, nil, proto)) + var byteOffset int + if len(args) > 1 && args[1] != nil && args[1] != _undefined { + byteOffset = r.toIndex(args[1]) + if byteOffset%ta.elemSize != 0 { + panic(r.newError(r.getRangeError(), "Start offset of %s should be a multiple of %d", newTarget.self.getStr("name", nil), ta.elemSize)) + } + } + var length int + if len(args) > 2 && args[2] != nil && args[2] != _undefined { + length = r.toIndex(args[2]) + ab.ensureNotDetached(true) + if byteOffset+length*ta.elemSize > len(ab.data) { + panic(r.newError(r.getRangeError(), "Invalid typed array length: %d", length)) + } + } else { + ab.ensureNotDetached(true) + if len(ab.data)%ta.elemSize != 0 { + panic(r.newError(r.getRangeError(), "Byte length of %s should be a multiple of %d", newTarget.self.getStr("name", nil), ta.elemSize)) + } + length = (len(ab.data) - byteOffset) / ta.elemSize + if length < 0 { + panic(r.newError(r.getRangeError(), "Start offset %d is outside the bounds of the buffer", byteOffset)) + } + } + ta.offset = byteOffset / ta.elemSize + ta.length = length + return ta.val +} + +func checkTypedArrayMixBigInt(src, dst *Object) { + srcType := src.self.getStr("name", nil).String() + if strings.HasPrefix(srcType, "Big") { + if !strings.HasPrefix(dst.self.getStr("name", nil).String(), "Big") { + panic(errMixBigIntType) + } + } +} + +func (r *Runtime) _newTypedArrayFromTypedArray(src *typedArrayObject, newTarget *Object, taCtor typedArrayObjectCtor, proto *Object) *Object { + dst := r.allocateTypedArray(newTarget, 0, taCtor, proto) + src.viewedArrayBuf.ensureNotDetached(true) + l := src.length + + dst.viewedArrayBuf.data = allocByteSlice(toIntStrict(int64(l) * int64(dst.elemSize))) + src.viewedArrayBuf.ensureNotDetached(true) + if src.defaultCtor == dst.defaultCtor { + copy(dst.viewedArrayBuf.data, src.viewedArrayBuf.data[src.offset*src.elemSize:]) + dst.length = src.length + return dst.val + } else { + checkTypedArrayMixBigInt(src.defaultCtor, newTarget) + } + dst.length = l + for i := 0; i < l; i++ { + dst.typedArray.set(i, src.typedArray.get(src.offset+i)) + } + return dst.val +} + +func (r *Runtime) _newTypedArray(args []Value, newTarget *Object, taCtor typedArrayObjectCtor, proto *Object) *Object { + if newTarget == nil { + panic(r.needNew("TypedArray")) + } + if len(args) > 0 { + if obj, ok := args[0].(*Object); ok { + switch o := obj.self.(type) { + case *arrayBufferObject: + return r._newTypedArrayFromArrayBuffer(o, args, newTarget, taCtor, proto) + case *typedArrayObject: + return r._newTypedArrayFromTypedArray(o, newTarget, taCtor, proto) + default: + return r.typedArrayFrom(newTarget, obj, nil, nil, taCtor, proto) + } + } + } + var l int + if len(args) > 0 { + if arg0 := args[0]; arg0 != nil { + l = r.toIndex(arg0) + } + } + return r.allocateTypedArray(newTarget, l, taCtor, proto).val +} + +func (r *Runtime) newUint8Array(args []Value, newTarget, proto *Object) *Object { + return r._newTypedArray(args, newTarget, r.newUint8ArrayObject, proto) +} + +func (r *Runtime) newUint8ClampedArray(args []Value, newTarget, proto *Object) *Object { + return r._newTypedArray(args, newTarget, r.newUint8ClampedArrayObject, proto) +} + +func (r *Runtime) newInt8Array(args []Value, newTarget, proto *Object) *Object { + return r._newTypedArray(args, newTarget, r.newInt8ArrayObject, proto) +} + +func (r *Runtime) newUint16Array(args []Value, newTarget, proto *Object) *Object { + return r._newTypedArray(args, newTarget, r.newUint16ArrayObject, proto) +} + +func (r *Runtime) newInt16Array(args []Value, newTarget, proto *Object) *Object { + return r._newTypedArray(args, newTarget, r.newInt16ArrayObject, proto) +} + +func (r *Runtime) newUint32Array(args []Value, newTarget, proto *Object) *Object { + return r._newTypedArray(args, newTarget, r.newUint32ArrayObject, proto) +} + +func (r *Runtime) newInt32Array(args []Value, newTarget, proto *Object) *Object { + return r._newTypedArray(args, newTarget, r.newInt32ArrayObject, proto) +} + +func (r *Runtime) newFloat32Array(args []Value, newTarget, proto *Object) *Object { + return r._newTypedArray(args, newTarget, r.newFloat32ArrayObject, proto) +} + +func (r *Runtime) newFloat64Array(args []Value, newTarget, proto *Object) *Object { + return r._newTypedArray(args, newTarget, r.newFloat64ArrayObject, proto) +} + +func (r *Runtime) newBigInt64Array(args []Value, newTarget, proto *Object) *Object { + return r._newTypedArray(args, newTarget, r.newBigInt64ArrayObject, proto) +} + +func (r *Runtime) newBigUint64Array(args []Value, newTarget, proto *Object) *Object { + return r._newTypedArray(args, newTarget, r.newBigUint64ArrayObject, proto) +} + +func (r *Runtime) createArrayBufferProto(val *Object) objectImpl { + b := newBaseObjectObj(val, r.global.ObjectPrototype, classObject) + byteLengthProp := &valueProperty{ + accessor: true, + configurable: true, + getterFunc: r.newNativeFunc(r.arrayBufferProto_getByteLength, "get byteLength", 0), + } + b._put("byteLength", byteLengthProp) + b._putProp("constructor", r.getArrayBuffer(), true, false, true) + b._putProp("slice", r.newNativeFunc(r.arrayBufferProto_slice, "slice", 2), true, false, true) + b._putSym(SymToStringTag, valueProp(asciiString("ArrayBuffer"), false, false, true)) + return b +} + +func (r *Runtime) createArrayBuffer(val *Object) objectImpl { + o := r.newNativeConstructOnly(val, r.builtin_newArrayBuffer, r.getArrayBufferPrototype(), "ArrayBuffer", 1) + o._putProp("isView", r.newNativeFunc(r.arrayBuffer_isView, "isView", 1), true, false, true) + r.putSpeciesReturnThis(o) + + return o +} + +func (r *Runtime) createDataView(val *Object) objectImpl { + o := r.newNativeConstructOnly(val, r.newDataView, r.getDataViewPrototype(), "DataView", 1) + return o +} + +func (r *Runtime) createTypedArray(val *Object) objectImpl { + o := r.newNativeConstructOnly(val, r.newTypedArray, r.getTypedArrayPrototype(), "TypedArray", 0) + o._putProp("from", r.newNativeFunc(r.typedArray_from, "from", 1), true, false, true) + o._putProp("of", r.newNativeFunc(r.typedArray_of, "of", 0), true, false, true) + r.putSpeciesReturnThis(o) + + return o +} + +func (r *Runtime) getTypedArray() *Object { + ret := r.global.TypedArray + if ret == nil { + ret = &Object{runtime: r} + r.global.TypedArray = ret + r.createTypedArray(ret) + } + return ret +} + +func (r *Runtime) createTypedArrayCtor(val *Object, ctor func(args []Value, newTarget, proto *Object) *Object, name unistring.String, bytesPerElement int) { + p := r.newBaseObject(r.getTypedArrayPrototype(), classObject) + o := r.newNativeConstructOnly(val, func(args []Value, newTarget *Object) *Object { + return ctor(args, newTarget, p.val) + }, p.val, name, 3) + + p._putProp("constructor", o.val, true, false, true) + + o.prototype = r.getTypedArray() + bpe := intToValue(int64(bytesPerElement)) + o._putProp("BYTES_PER_ELEMENT", bpe, false, false, false) + p._putProp("BYTES_PER_ELEMENT", bpe, false, false, false) +} + +func addTypedArrays(t *objectTemplate) { + t.putStr("ArrayBuffer", func(r *Runtime) Value { return valueProp(r.getArrayBuffer(), true, false, true) }) + t.putStr("DataView", func(r *Runtime) Value { return valueProp(r.getDataView(), true, false, true) }) + t.putStr("Uint8Array", func(r *Runtime) Value { return valueProp(r.getUint8Array(), true, false, true) }) + t.putStr("Uint8ClampedArray", func(r *Runtime) Value { return valueProp(r.getUint8ClampedArray(), true, false, true) }) + t.putStr("Int8Array", func(r *Runtime) Value { return valueProp(r.getInt8Array(), true, false, true) }) + t.putStr("Uint16Array", func(r *Runtime) Value { return valueProp(r.getUint16Array(), true, false, true) }) + t.putStr("Int16Array", func(r *Runtime) Value { return valueProp(r.getInt16Array(), true, false, true) }) + t.putStr("Uint32Array", func(r *Runtime) Value { return valueProp(r.getUint32Array(), true, false, true) }) + t.putStr("Int32Array", func(r *Runtime) Value { return valueProp(r.getInt32Array(), true, false, true) }) + t.putStr("Float32Array", func(r *Runtime) Value { return valueProp(r.getFloat32Array(), true, false, true) }) + t.putStr("Float64Array", func(r *Runtime) Value { return valueProp(r.getFloat64Array(), true, false, true) }) + t.putStr("BigInt64Array", func(r *Runtime) Value { return valueProp(r.getBigInt64Array(), true, false, true) }) + t.putStr("BigUint64Array", func(r *Runtime) Value { return valueProp(r.getBigUint64Array(), true, false, true) }) +} + +func createTypedArrayProtoTemplate() *objectTemplate { + t := newObjectTemplate() + t.protoFactory = func(r *Runtime) *Object { + return r.global.ObjectPrototype + } + + t.putStr("buffer", func(r *Runtime) Value { + return &valueProperty{ + accessor: true, + configurable: true, + getterFunc: r.newNativeFunc(r.typedArrayProto_getBuffer, "get buffer", 0), + } + }) + + t.putStr("byteLength", func(r *Runtime) Value { + return &valueProperty{ + accessor: true, + configurable: true, + getterFunc: r.newNativeFunc(r.typedArrayProto_getByteLen, "get byteLength", 0), + } + }) + + t.putStr("byteOffset", func(r *Runtime) Value { + return &valueProperty{ + accessor: true, + configurable: true, + getterFunc: r.newNativeFunc(r.typedArrayProto_getByteOffset, "get byteOffset", 0), + } + }) + + t.putStr("at", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_at, "at", 1) }) + t.putStr("constructor", func(r *Runtime) Value { return valueProp(r.getTypedArray(), true, false, true) }) + t.putStr("copyWithin", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_copyWithin, "copyWithin", 2) }) + t.putStr("entries", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_entries, "entries", 0) }) + t.putStr("every", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_every, "every", 1) }) + t.putStr("fill", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_fill, "fill", 1) }) + t.putStr("filter", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_filter, "filter", 1) }) + t.putStr("find", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_find, "find", 1) }) + t.putStr("findIndex", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_findIndex, "findIndex", 1) }) + t.putStr("findLast", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_findLast, "findLast", 1) }) + t.putStr("findLastIndex", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_findLastIndex, "findLastIndex", 1) }) + t.putStr("forEach", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_forEach, "forEach", 1) }) + t.putStr("includes", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_includes, "includes", 1) }) + t.putStr("indexOf", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_indexOf, "indexOf", 1) }) + t.putStr("join", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_join, "join", 1) }) + t.putStr("keys", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_keys, "keys", 0) }) + t.putStr("lastIndexOf", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_lastIndexOf, "lastIndexOf", 1) }) + t.putStr("length", func(r *Runtime) Value { + return &valueProperty{ + accessor: true, + configurable: true, + getterFunc: r.newNativeFunc(r.typedArrayProto_getLength, "get length", 0), + } + }) + t.putStr("map", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_map, "map", 1) }) + t.putStr("reduce", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_reduce, "reduce", 1) }) + t.putStr("reduceRight", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_reduceRight, "reduceRight", 1) }) + t.putStr("reverse", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_reverse, "reverse", 0) }) + t.putStr("set", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_set, "set", 1) }) + t.putStr("slice", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_slice, "slice", 2) }) + t.putStr("some", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_some, "some", 1) }) + t.putStr("sort", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_sort, "sort", 1) }) + t.putStr("subarray", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_subarray, "subarray", 2) }) + t.putStr("toLocaleString", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_toLocaleString, "toLocaleString", 0) }) + t.putStr("with", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_with, "with", 2) }) + t.putStr("toReversed", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_toReversed, "toReversed", 0) }) + t.putStr("toSorted", func(r *Runtime) Value { return r.methodProp(r.typedArrayProto_toSorted, "toSorted", 1) }) + t.putStr("toString", func(r *Runtime) Value { return valueProp(r.getArrayToString(), true, false, true) }) + t.putStr("values", func(r *Runtime) Value { return valueProp(r.getTypedArrayValues(), true, false, true) }) + + t.putSym(SymIterator, func(r *Runtime) Value { return valueProp(r.getTypedArrayValues(), true, false, true) }) + t.putSym(SymToStringTag, func(r *Runtime) Value { + return &valueProperty{ + getterFunc: r.newNativeFunc(r.typedArrayProto_toStringTag, "get [Symbol.toStringTag]", 0), + accessor: true, + configurable: true, + } + }) + + return t +} + +func (r *Runtime) getTypedArrayValues() *Object { + ret := r.global.typedArrayValues + if ret == nil { + ret = r.newNativeFunc(r.typedArrayProto_values, "values", 0) + r.global.typedArrayValues = ret + } + return ret +} + +var typedArrayProtoTemplate *objectTemplate +var typedArrayProtoTemplateOnce sync.Once + +func getTypedArrayProtoTemplate() *objectTemplate { + typedArrayProtoTemplateOnce.Do(func() { + typedArrayProtoTemplate = createTypedArrayProtoTemplate() + }) + return typedArrayProtoTemplate +} + +func (r *Runtime) getTypedArrayPrototype() *Object { + ret := r.global.TypedArrayPrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.TypedArrayPrototype = ret + r.newTemplatedObject(getTypedArrayProtoTemplate(), ret) + } + return ret +} + +func (r *Runtime) getUint8Array() *Object { + ret := r.global.Uint8Array + if ret == nil { + ret = &Object{runtime: r} + r.global.Uint8Array = ret + r.createTypedArrayCtor(ret, r.newUint8Array, "Uint8Array", 1) + } + return ret +} + +func (r *Runtime) getUint8ClampedArray() *Object { + ret := r.global.Uint8ClampedArray + if ret == nil { + ret = &Object{runtime: r} + r.global.Uint8ClampedArray = ret + r.createTypedArrayCtor(ret, r.newUint8ClampedArray, "Uint8ClampedArray", 1) + } + return ret +} + +func (r *Runtime) getInt8Array() *Object { + ret := r.global.Int8Array + if ret == nil { + ret = &Object{runtime: r} + r.global.Int8Array = ret + r.createTypedArrayCtor(ret, r.newInt8Array, "Int8Array", 1) + } + return ret +} + +func (r *Runtime) getUint16Array() *Object { + ret := r.global.Uint16Array + if ret == nil { + ret = &Object{runtime: r} + r.global.Uint16Array = ret + r.createTypedArrayCtor(ret, r.newUint16Array, "Uint16Array", 2) + } + return ret +} + +func (r *Runtime) getInt16Array() *Object { + ret := r.global.Int16Array + if ret == nil { + ret = &Object{runtime: r} + r.global.Int16Array = ret + r.createTypedArrayCtor(ret, r.newInt16Array, "Int16Array", 2) + } + return ret +} + +func (r *Runtime) getUint32Array() *Object { + ret := r.global.Uint32Array + if ret == nil { + ret = &Object{runtime: r} + r.global.Uint32Array = ret + r.createTypedArrayCtor(ret, r.newUint32Array, "Uint32Array", 4) + } + return ret +} + +func (r *Runtime) getInt32Array() *Object { + ret := r.global.Int32Array + if ret == nil { + ret = &Object{runtime: r} + r.global.Int32Array = ret + r.createTypedArrayCtor(ret, r.newInt32Array, "Int32Array", 4) + } + return ret +} + +func (r *Runtime) getFloat32Array() *Object { + ret := r.global.Float32Array + if ret == nil { + ret = &Object{runtime: r} + r.global.Float32Array = ret + r.createTypedArrayCtor(ret, r.newFloat32Array, "Float32Array", 4) + } + return ret +} + +func (r *Runtime) getFloat64Array() *Object { + ret := r.global.Float64Array + if ret == nil { + ret = &Object{runtime: r} + r.global.Float64Array = ret + r.createTypedArrayCtor(ret, r.newFloat64Array, "Float64Array", 8) + } + return ret +} + +func (r *Runtime) getBigInt64Array() *Object { + ret := r.global.BigInt64Array + if ret == nil { + ret = &Object{runtime: r} + r.global.BigInt64Array = ret + r.createTypedArrayCtor(ret, r.newBigInt64Array, "BigInt64Array", 8) + } + return ret +} + +func (r *Runtime) getBigUint64Array() *Object { + ret := r.global.BigUint64Array + if ret == nil { + ret = &Object{runtime: r} + r.global.BigUint64Array = ret + r.createTypedArrayCtor(ret, r.newBigUint64Array, "BigUint64Array", 8) + } + return ret +} + +func createDataViewProtoTemplate() *objectTemplate { + t := newObjectTemplate() + t.protoFactory = func(r *Runtime) *Object { + return r.global.ObjectPrototype + } + + t.putStr("buffer", func(r *Runtime) Value { + return &valueProperty{ + accessor: true, + configurable: true, + getterFunc: r.newNativeFunc(r.dataViewProto_getBuffer, "get buffer", 0), + } + }) + t.putStr("byteLength", func(r *Runtime) Value { + return &valueProperty{ + accessor: true, + configurable: true, + getterFunc: r.newNativeFunc(r.dataViewProto_getByteLen, "get byteLength", 0), + } + }) + t.putStr("byteOffset", func(r *Runtime) Value { + return &valueProperty{ + accessor: true, + configurable: true, + getterFunc: r.newNativeFunc(r.dataViewProto_getByteOffset, "get byteOffset", 0), + } + }) + + t.putStr("constructor", func(r *Runtime) Value { return valueProp(r.getDataView(), true, false, true) }) + + t.putStr("getFloat32", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_getFloat32, "getFloat32", 1) }) + t.putStr("getFloat64", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_getFloat64, "getFloat64", 1) }) + t.putStr("getInt8", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_getInt8, "getInt8", 1) }) + t.putStr("getInt16", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_getInt16, "getInt16", 1) }) + t.putStr("getInt32", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_getInt32, "getInt32", 1) }) + t.putStr("getUint8", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_getUint8, "getUint8", 1) }) + t.putStr("getUint16", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_getUint16, "getUint16", 1) }) + t.putStr("getUint32", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_getUint32, "getUint32", 1) }) + t.putStr("getBigInt64", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_getBigInt64, "getBigInt64", 1) }) + t.putStr("getBigUint64", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_getBigUint64, "getBigUint64", 1) }) + t.putStr("setFloat32", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_setFloat32, "setFloat32", 2) }) + t.putStr("setFloat64", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_setFloat64, "setFloat64", 2) }) + t.putStr("setInt8", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_setInt8, "setInt8", 2) }) + t.putStr("setInt16", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_setInt16, "setInt16", 2) }) + t.putStr("setInt32", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_setInt32, "setInt32", 2) }) + t.putStr("setUint8", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_setUint8, "setUint8", 2) }) + t.putStr("setUint16", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_setUint16, "setUint16", 2) }) + t.putStr("setUint32", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_setUint32, "setUint32", 2) }) + t.putStr("setBigInt64", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_setBigInt64, "setBigInt64", 2) }) + t.putStr("setBigUint64", func(r *Runtime) Value { return r.methodProp(r.dataViewProto_setBigUint64, "setBigUint64", 2) }) + + t.putSym(SymToStringTag, func(r *Runtime) Value { return valueProp(asciiString("DataView"), false, false, true) }) + + return t +} + +var dataViewProtoTemplate *objectTemplate +var dataViewProtoTemplateOnce sync.Once + +func getDataViewProtoTemplate() *objectTemplate { + dataViewProtoTemplateOnce.Do(func() { + dataViewProtoTemplate = createDataViewProtoTemplate() + }) + return dataViewProtoTemplate +} + +func (r *Runtime) getDataViewPrototype() *Object { + ret := r.global.DataViewPrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.DataViewPrototype = ret + r.newTemplatedObject(getDataViewProtoTemplate(), ret) + } + return ret +} + +func (r *Runtime) getDataView() *Object { + ret := r.global.DataView + if ret == nil { + ret = &Object{runtime: r} + r.global.DataView = ret + ret.self = r.createDataView(ret) + } + return ret +} + +func (r *Runtime) getArrayBufferPrototype() *Object { + ret := r.global.ArrayBufferPrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.ArrayBufferPrototype = ret + ret.self = r.createArrayBufferProto(ret) + } + return ret +} + +func (r *Runtime) getArrayBuffer() *Object { + ret := r.global.ArrayBuffer + if ret == nil { + ret = &Object{runtime: r} + r.global.ArrayBuffer = ret + ret.self = r.createArrayBuffer(ret) + } + return ret +} diff --git a/goja/builtin_typedarrays_test.go b/goja/builtin_typedarrays_test.go new file mode 100644 index 0000000..ad9f6d8 --- /dev/null +++ b/goja/builtin_typedarrays_test.go @@ -0,0 +1,326 @@ +package goja + +import ( + "testing" +) + +/* +func TestArrayBufferNew(t *testing.T) { + const SCRIPT = ` + var b = new ArrayBuffer(16); + b.byteLength; + ` + + testScript(SCRIPT, intToValue(16), t) +} +*/ + +func TestArrayBufferSetUint32(t *testing.T) { + vm := New() + b := vm._newArrayBuffer(vm.global.ArrayBufferPrototype, nil) + b.data = make([]byte, 4) + b.setUint32(0, 0xCAFEBABE, bigEndian) + + i := b.getUint32(0, bigEndian) + if i != 0xCAFEBABE { + t.Fatal(i) + } + i = b.getUint32(0, littleEndian) + if i != 0xBEBAFECA { + t.Fatal(i) + } + + b.setUint32(0, 0xBEBAFECA, littleEndian) + i = b.getUint32(0, bigEndian) + if i != 0xCAFEBABE { + t.Fatal(i) + } +} + +func TestArrayBufferSetInt32(t *testing.T) { + vm := New() + b := vm._newArrayBuffer(vm.global.ArrayBufferPrototype, nil) + b.data = make([]byte, 4) + b.setInt32(0, -42, littleEndian) + if v := b.getInt32(0, littleEndian); v != -42 { + t.Fatal(v) + } + + b.setInt32(0, -42, bigEndian) + if v := b.getInt32(0, bigEndian); v != -42 { + t.Fatal(v) + } +} + +func TestNewUint8Array(t *testing.T) { + const SCRIPT = ` + var a = new Uint8Array(1); + a[0] = 42; + a.byteLength === 1 && a.length === 1 && a[0] === 42; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestNewUint16Array(t *testing.T) { + const SCRIPT = ` + var a = new Uint16Array(1); + a[0] = 42; + a.byteLength === 2 && a.length === 1 && a[0] === 42; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestTypedArraysSpeciesConstructor(t *testing.T) { + const SCRIPT = ` + 'use strict'; + function MyArray() { + var NewTarget = this.__proto__.constructor; + return Reflect.construct(Uint16Array, arguments, NewTarget); + } + MyArray.prototype = Object.create(Uint16Array.prototype, { + constructor: { + value: MyArray, + writable: true, + configurable: true + } + }); + var a = new MyArray(1); + Object.defineProperty(MyArray, Symbol.species, {value: Uint8Array, configurable: true}); + a[0] = 32767; + var b = a.filter(function() { + return true; + }); + if (a[0] !== 32767) { + throw new Error("a[0]=" + a[0]); + } + if (!(b instanceof Uint8Array)) { + throw new Error("b instanceof Uint8Array"); + } + if (b[0] != 255) { + throw new Error("b[0]=" + b[0]); + } + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestTypedArrayFromArrayBuffer(t *testing.T) { + const SCRIPT = ` + var buf = new ArrayBuffer(2); + var a16 = new Uint16Array(buf); + if (!(a16 instanceof Uint16Array)) { + throw new Error("a16 is not an instance"); + } + if (a16.buffer !== buf) { + throw new Error("a16.buffer !== buf"); + } + if (a16.length !== 1) { + throw new Error("a16.length=" + a16.length); + } + var a8 = new Uint8Array(buf); + a8.fill(0xAA); + if (a16[0] !== 0xAAAA) { + throw new Error("a16[0]=" + a16[0]); + } + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestTypedArraySetOverlapDifSize(t *testing.T) { + const SCRIPT = ` + var buf = new ArrayBuffer(4); + var src = new Uint8Array(buf, 1, 2); + src[0] = 1; + src[1] = 2; + var dst = new Uint16Array(buf); + dst.set(src); + if (dst[0] !== 1 || dst[1] !== 2) { + throw new Error("dst: " + dst.join(",")); + } + ` + testScript(SCRIPT, _undefined, t) +} + +func TestTypedArraySetOverlapDifSize2(t *testing.T) { + const SCRIPT = ` + var buf = new ArrayBuffer(4); + var src = new Uint8Array(buf, 0, 2); + src[0] = 1; + src[1] = 2; + var dst = new Uint16Array(buf); + dst.set(src); + if (dst[0] !== 1 || dst[1] !== 2) { + throw new Error("dst: " + dst.join(",")); + } + ` + testScript(SCRIPT, _undefined, t) +} + +func TestTypedArraySetOverlapDifSize3(t *testing.T) { + const SCRIPT = ` + var buf = new ArrayBuffer(8); + var src = new Uint8Array(buf, 2, 4); + src[0] = 1; + src[1] = 2; + src[2] = 3; + src[3] = 4; + var dst = new Uint16Array(buf); + dst.set(src); + if (dst[0] !== 1 || dst[1] !== 2 || dst[2] !== 3 || dst[3] !== 4) { + throw new Error("dst: " + dst.join(",")); + } + ` + testScript(SCRIPT, _undefined, t) +} + +func TestTypedArraySetOverlapDifSize4(t *testing.T) { + const SCRIPT = ` + var buf = new ArrayBuffer(10); + var dst = new Uint8Array(buf, 2, 5); + var src = new Uint16Array(buf); + src[0] = 1; + src[1] = 2; + src[2] = 3; + src[3] = 4; + src[4] = 5; + dst.set(src); + if (dst[0] !== 1 || dst[1] !== 2 || dst[2] !== 3 || dst[3] !== 4 || dst[4] !== 5) { + throw new Error("dst: " + dst.join(",")); + } + ` + testScript(SCRIPT, _undefined, t) +} + +func TestTypedArraySetNoOverlapDifSizeForward(t *testing.T) { + const SCRIPT = ` + var buf = new ArrayBuffer(10); + var dst = new Uint8Array(buf, 7, 2); + var src = new Uint16Array(buf, 0, 2); + src[0] = 1; + src[1] = 2; + dst.set(src); + if (dst[0] !== 1 || dst[1] !== 2 || src[0] !== 1 || src[1] !== 2) { + throw new Error("dst: " + dst.join(",")); + } + ` + testScript(SCRIPT, _undefined, t) +} + +func TestTypedArraySetNoOverlapDifSizeBackward(t *testing.T) { + const SCRIPT = ` + var buf = new ArrayBuffer(10); + var dst = new Uint8Array(buf, 0, 2); + var src = new Uint16Array(buf, 6, 2); + src[0] = 1; + src[1] = 2; + dst.set(src); + if (dst[0] !== 1 || dst[1] !== 2 || src[0] !== 1 || src[1] !== 2) { + throw new Error("dst: " + dst.join(",")); + } + ` + testScript(SCRIPT, _undefined, t) +} + +func TestTypedArraySetNoOverlapDifSizeDifBuffers(t *testing.T) { + const SCRIPT = ` + var dstBuf = new ArrayBuffer(1024); + var dst = new Uint8Array(dstBuf, 0, 2); + var src = new Uint16Array(2); + src[0] = 1; + src[1] = 2; + dst.set(src); + if (dst[0] !== 1 || dst[1] !== 2 || src[0] !== 1 || src[1] !== 2) { + throw new Error("dst: " + dst.join(",")); + } + ` + testScript(SCRIPT, _undefined, t) +} + +func TestTypedArraySliceSameType(t *testing.T) { + const SCRIPT = ` + var src = Uint8Array.of(1,2,3,4); + var dst = src.slice(1, 3); + if (dst.length !== 2 || dst[0] !== 2 || dst[1] !== 3) { + throw new Error("dst: " + dst.join(",")); + } + ` + testScript(SCRIPT, _undefined, t) +} + +func TestTypedArraySliceDifType(t *testing.T) { + const SCRIPT = ` + var src = Uint8Array.of(1,2,3,4); + Object.defineProperty(Uint8Array, Symbol.species, {value: Uint16Array, configurable: true}); + var dst = src.slice(1, 3); + if (!(dst instanceof Uint16Array)) { + throw new Error("wrong dst type: " + dst); + } + if (dst.length !== 2 || dst[0] !== 2 || dst[1] !== 3) { + throw new Error("dst: " + dst.join(",")); + } + ` + testScript(SCRIPT, _undefined, t) +} + +func TestTypedArraySortComparatorReturnValueFloats(t *testing.T) { + const SCRIPT = ` + var a = Float64Array.of( + 5.97, + 9.91, + 4.13, + 9.28, + 3.29 + ); + a.sort( function(a, b) { return a - b; } ); + for (var i = 1; i < a.length; i++) { + if (a[i] < a[i-1]) { + throw new Error("Array is not sorted: " + a); + } + } + ` + testScript(SCRIPT, _undefined, t) +} + +func TestTypedArraySortComparatorReturnValueNegZero(t *testing.T) { + const SCRIPT = ` + var a = new Uint8Array([2, 1]); + a.sort( function(a, b) { return a > b ? 0 : -0; } ); + for (var i = 1; i < a.length; i++) { + if (a[i] < a[i-1]) { + throw new Error("Array is not sorted: " + a); + } + } + ` + testScript(SCRIPT, _undefined, t) +} + +func TestInt32ArrayNegativeIndex(t *testing.T) { + const SCRIPT = ` + new Int32Array()[-1] === undefined; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestTypedArrayDeleteUnconfigurable(t *testing.T) { + const SCRIPT = ` + try { + (function() { + 'use strict'; + delete Uint8Array.prototype.BYTES_PER_ELEMENT; + })(); + } catch(e) { + if (!(e instanceof TypeError)) { + throw e; + } + if (!e.message.startsWith("Cannot delete property")) { + throw e; + } + } + ` + + testScript(SCRIPT, _undefined, t) +} diff --git a/goja/builtin_weakmap.go b/goja/builtin_weakmap.go new file mode 100644 index 0000000..40fc717 --- /dev/null +++ b/goja/builtin_weakmap.go @@ -0,0 +1,176 @@ +package goja + +type weakMap uint64 + +type weakMapObject struct { + baseObject + m weakMap +} + +func (wmo *weakMapObject) init() { + wmo.baseObject.init() + wmo.m = weakMap(wmo.val.runtime.genId()) +} + +func (wm weakMap) set(key *Object, value Value) { + key.getWeakRefs()[wm] = value +} + +func (wm weakMap) get(key *Object) Value { + return key.weakRefs[wm] +} + +func (wm weakMap) remove(key *Object) bool { + if _, exists := key.weakRefs[wm]; exists { + delete(key.weakRefs, wm) + return true + } + return false +} + +func (wm weakMap) has(key *Object) bool { + _, exists := key.weakRefs[wm] + return exists +} + +func (r *Runtime) weakMapProto_delete(call FunctionCall) Value { + thisObj := r.toObject(call.This) + wmo, ok := thisObj.self.(*weakMapObject) + if !ok { + panic(r.NewTypeError("Method WeakMap.prototype.delete called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + key, ok := call.Argument(0).(*Object) + if ok && wmo.m.remove(key) { + return valueTrue + } + return valueFalse +} + +func (r *Runtime) weakMapProto_get(call FunctionCall) Value { + thisObj := r.toObject(call.This) + wmo, ok := thisObj.self.(*weakMapObject) + if !ok { + panic(r.NewTypeError("Method WeakMap.prototype.get called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + var res Value + if key, ok := call.Argument(0).(*Object); ok { + res = wmo.m.get(key) + } + if res == nil { + return _undefined + } + return res +} + +func (r *Runtime) weakMapProto_has(call FunctionCall) Value { + thisObj := r.toObject(call.This) + wmo, ok := thisObj.self.(*weakMapObject) + if !ok { + panic(r.NewTypeError("Method WeakMap.prototype.has called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + key, ok := call.Argument(0).(*Object) + if ok && wmo.m.has(key) { + return valueTrue + } + return valueFalse +} + +func (r *Runtime) weakMapProto_set(call FunctionCall) Value { + thisObj := r.toObject(call.This) + wmo, ok := thisObj.self.(*weakMapObject) + if !ok { + panic(r.NewTypeError("Method WeakMap.prototype.set called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + key := r.toObject(call.Argument(0)) + wmo.m.set(key, call.Argument(1)) + return call.This +} + +func (r *Runtime) needNew(name string) *Object { + return r.NewTypeError("Constructor %s requires 'new'", name) +} + +func (r *Runtime) builtin_newWeakMap(args []Value, newTarget *Object) *Object { + if newTarget == nil { + panic(r.needNew("WeakMap")) + } + proto := r.getPrototypeFromCtor(newTarget, r.global.WeakMap, r.global.WeakMapPrototype) + o := &Object{runtime: r} + + wmo := &weakMapObject{} + wmo.class = classObject + wmo.val = o + wmo.extensible = true + o.self = wmo + wmo.prototype = proto + wmo.init() + if len(args) > 0 { + if arg := args[0]; arg != nil && arg != _undefined && arg != _null { + adder := wmo.getStr("set", nil) + adderFn := toMethod(adder) + if adderFn == nil { + panic(r.NewTypeError("WeakMap.set in missing")) + } + iter := r.getIterator(arg, nil) + i0 := valueInt(0) + i1 := valueInt(1) + if adder == r.global.weakMapAdder { + iter.iterate(func(item Value) { + itemObj := r.toObject(item) + k := itemObj.self.getIdx(i0, nil) + v := nilSafe(itemObj.self.getIdx(i1, nil)) + wmo.m.set(r.toObject(k), v) + }) + } else { + iter.iterate(func(item Value) { + itemObj := r.toObject(item) + k := itemObj.self.getIdx(i0, nil) + v := itemObj.self.getIdx(i1, nil) + adderFn(FunctionCall{This: o, Arguments: []Value{k, v}}) + }) + } + } + } + return o +} + +func (r *Runtime) createWeakMapProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.global.ObjectPrototype, classObject) + + o._putProp("constructor", r.getWeakMap(), true, false, true) + r.global.weakMapAdder = r.newNativeFunc(r.weakMapProto_set, "set", 2) + o._putProp("set", r.global.weakMapAdder, true, false, true) + o._putProp("delete", r.newNativeFunc(r.weakMapProto_delete, "delete", 1), true, false, true) + o._putProp("has", r.newNativeFunc(r.weakMapProto_has, "has", 1), true, false, true) + o._putProp("get", r.newNativeFunc(r.weakMapProto_get, "get", 1), true, false, true) + + o._putSym(SymToStringTag, valueProp(asciiString(classWeakMap), false, false, true)) + + return o +} + +func (r *Runtime) createWeakMap(val *Object) objectImpl { + o := r.newNativeConstructOnly(val, r.builtin_newWeakMap, r.getWeakMapPrototype(), "WeakMap", 0) + + return o +} + +func (r *Runtime) getWeakMapPrototype() *Object { + ret := r.global.WeakMapPrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.WeakMapPrototype = ret + ret.self = r.createWeakMapProto(ret) + } + return ret +} + +func (r *Runtime) getWeakMap() *Object { + ret := r.global.WeakMap + if ret == nil { + ret = &Object{runtime: r} + r.global.WeakMap = ret + ret.self = r.createWeakMap(ret) + } + return ret +} diff --git a/goja/builtin_weakmap_test.go b/goja/builtin_weakmap_test.go new file mode 100644 index 0000000..648320d --- /dev/null +++ b/goja/builtin_weakmap_test.go @@ -0,0 +1,76 @@ +package goja + +import ( + "testing" +) + +func TestWeakMap(t *testing.T) { + vm := New() + _, err := vm.RunString(` + var m = new WeakMap(); + var m1 = new WeakMap(); + var key = {}; + m.set(key, true); + m1.set(key, false); + if (!m.has(key)) { + throw new Error("has"); + } + if (m.get(key) !== true) { + throw new Error("value does not match"); + } + if (!m1.has(key)) { + throw new Error("has (m1)"); + } + if (m1.get(key) !== false) { + throw new Error("m1 value does not match"); + } + m.delete(key); + if (m.has(key)) { + throw new Error("m still has after delete"); + } + if (!m1.has(key)) { + throw new Error("m1 does not have after delete from m"); + } + `) + if err != nil { + t.Fatal(err) + } +} + +func TestWeakMapGetAdderGetIteratorOrder(t *testing.T) { + const SCRIPT = ` + let getterCalled = 0; + + class M extends WeakMap { + get set() { + getterCalled++; + return null; + } + } + + let getIteratorCalled = 0; + + let iterable = {}; + iterable[Symbol.iterator] = () => { + getIteratorCalled++ + return { + next: 1 + }; + } + + let thrown = false; + + try { + new M(iterable); + } catch (e) { + if (e instanceof TypeError) { + thrown = true; + } else { + throw e; + } + } + + thrown && getterCalled === 1 && getIteratorCalled === 0; + ` + testScript(SCRIPT, valueTrue, t) +} diff --git a/goja/builtin_weakset.go b/goja/builtin_weakset.go new file mode 100644 index 0000000..cd8183e --- /dev/null +++ b/goja/builtin_weakset.go @@ -0,0 +1,135 @@ +package goja + +type weakSetObject struct { + baseObject + s weakMap +} + +func (ws *weakSetObject) init() { + ws.baseObject.init() + ws.s = weakMap(ws.val.runtime.genId()) +} + +func (r *Runtime) weakSetProto_add(call FunctionCall) Value { + thisObj := r.toObject(call.This) + wso, ok := thisObj.self.(*weakSetObject) + if !ok { + panic(r.NewTypeError("Method WeakSet.prototype.add called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + wso.s.set(r.toObject(call.Argument(0)), nil) + return call.This +} + +func (r *Runtime) weakSetProto_delete(call FunctionCall) Value { + thisObj := r.toObject(call.This) + wso, ok := thisObj.self.(*weakSetObject) + if !ok { + panic(r.NewTypeError("Method WeakSet.prototype.delete called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + obj, ok := call.Argument(0).(*Object) + if ok && wso.s.remove(obj) { + return valueTrue + } + return valueFalse +} + +func (r *Runtime) weakSetProto_has(call FunctionCall) Value { + thisObj := r.toObject(call.This) + wso, ok := thisObj.self.(*weakSetObject) + if !ok { + panic(r.NewTypeError("Method WeakSet.prototype.has called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) + } + obj, ok := call.Argument(0).(*Object) + if ok && wso.s.has(obj) { + return valueTrue + } + return valueFalse +} + +func (r *Runtime) builtin_newWeakSet(args []Value, newTarget *Object) *Object { + if newTarget == nil { + panic(r.needNew("WeakSet")) + } + proto := r.getPrototypeFromCtor(newTarget, r.global.WeakSet, r.global.WeakSetPrototype) + o := &Object{runtime: r} + + wso := &weakSetObject{} + wso.class = classObject + wso.val = o + wso.extensible = true + o.self = wso + wso.prototype = proto + wso.init() + if len(args) > 0 { + if arg := args[0]; arg != nil && arg != _undefined && arg != _null { + adder := wso.getStr("add", nil) + stdArr := r.checkStdArrayIter(arg) + if adder == r.global.weakSetAdder { + if stdArr != nil { + for _, v := range stdArr.values { + wso.s.set(r.toObject(v), nil) + } + } else { + r.getIterator(arg, nil).iterate(func(item Value) { + wso.s.set(r.toObject(item), nil) + }) + } + } else { + adderFn := toMethod(adder) + if adderFn == nil { + panic(r.NewTypeError("WeakSet.add in missing")) + } + if stdArr != nil { + for _, item := range stdArr.values { + adderFn(FunctionCall{This: o, Arguments: []Value{item}}) + } + } else { + r.getIterator(arg, nil).iterate(func(item Value) { + adderFn(FunctionCall{This: o, Arguments: []Value{item}}) + }) + } + } + } + } + return o +} + +func (r *Runtime) createWeakSetProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.global.ObjectPrototype, classObject) + + o._putProp("constructor", r.global.WeakSet, true, false, true) + r.global.weakSetAdder = r.newNativeFunc(r.weakSetProto_add, "add", 1) + o._putProp("add", r.global.weakSetAdder, true, false, true) + o._putProp("delete", r.newNativeFunc(r.weakSetProto_delete, "delete", 1), true, false, true) + o._putProp("has", r.newNativeFunc(r.weakSetProto_has, "has", 1), true, false, true) + + o._putSym(SymToStringTag, valueProp(asciiString(classWeakSet), false, false, true)) + + return o +} + +func (r *Runtime) createWeakSet(val *Object) objectImpl { + o := r.newNativeConstructOnly(val, r.builtin_newWeakSet, r.getWeakSetPrototype(), "WeakSet", 0) + + return o +} + +func (r *Runtime) getWeakSetPrototype() *Object { + ret := r.global.WeakSetPrototype + if ret == nil { + ret = &Object{runtime: r} + r.global.WeakSetPrototype = ret + ret.self = r.createWeakSetProto(ret) + } + return ret +} + +func (r *Runtime) getWeakSet() *Object { + ret := r.global.WeakSet + if ret == nil { + ret = &Object{runtime: r} + r.global.WeakSet = ret + ret.self = r.createWeakSet(ret) + } + return ret +} diff --git a/goja/builtin_weakset_test.go b/goja/builtin_weakset_test.go new file mode 100644 index 0000000..edd1b17 --- /dev/null +++ b/goja/builtin_weakset_test.go @@ -0,0 +1,101 @@ +package goja + +import ( + "testing" +) + +func TestWeakSetBasic(t *testing.T) { + const SCRIPT = ` + var s = new WeakSet(); + var o = {}; + s.add(o); + if (!s.has(o)) { + throw new Error("has"); + } + s.delete(o); + if (s.has(o)) { + throw new Error("still has"); + } + ` + testScript(SCRIPT, _undefined, t) +} + +func TestWeakSetArraySimple(t *testing.T) { + const SCRIPT = ` + var o1 = {}, o2 = {}, o3 = {}; + + var s = new WeakSet([o1, o2, o3]); + s.has(o1) && s.has(o2) && s.has(o3); + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestWeakSetArrayGeneric(t *testing.T) { + const SCRIPT = ` + var o1 = {}, o2 = {}, o3 = {}; + var a = new Array(); + var s; + var thrown = false; + a[1] = o2; + + try { + s = new WeakSet(a); + } catch (e) { + if (e instanceof TypeError) { + thrown = true; + } + } + if (!thrown) { + throw new Error("Case 1 does not throw"); + } + + Object.defineProperty(a.__proto__, "0", {value: o1, writable: true, enumerable: true, configurable: true}); + s = new WeakSet(a); + if (!(s.has(o1) && s.has(o2) && !s.has(o3))) { + throw new Error("Case 2 failed"); + } + + Object.defineProperty(a, "2", {value: o3, configurable: true}); + s = new WeakSet(a); + s.has(o1) && s.has(o2) && s.has(o3); + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestWeakSetGetAdderGetIteratorOrder(t *testing.T) { + const SCRIPT = ` + let getterCalled = 0; + + class S extends WeakSet { + get add() { + getterCalled++; + return null; + } + } + + let getIteratorCalled = 0; + + let iterable = {}; + iterable[Symbol.iterator] = () => { + getIteratorCalled++ + return { + next: 1 + }; + } + + let thrown = false; + + try { + new S(iterable); + } catch (e) { + if (e instanceof TypeError) { + thrown = true; + } else { + throw e; + } + } + + thrown && getterCalled === 1 && getIteratorCalled === 0; + ` + testScript(SCRIPT, valueTrue, t) +} diff --git a/goja/compiler.go b/goja/compiler.go new file mode 100644 index 0000000..2abd9ba --- /dev/null +++ b/goja/compiler.go @@ -0,0 +1,1481 @@ +package goja + +import ( + "fmt" + "github.com/dop251/goja/token" + "sort" + + "github.com/dop251/goja/ast" + "github.com/dop251/goja/file" + "github.com/dop251/goja/unistring" +) + +type blockType int + +const ( + blockLoop blockType = iota + blockLoopEnum + blockTry + blockLabel + blockSwitch + blockWith + blockScope + blockIterScope + blockOptChain +) + +const ( + maskConst = 1 << 31 + maskVar = 1 << 30 + maskDeletable = 1 << 29 + maskStrict = maskDeletable + + maskTyp = maskConst | maskVar | maskDeletable +) + +type varType byte + +const ( + varTypeVar varType = iota + varTypeLet + varTypeStrictConst + varTypeConst +) + +const thisBindingName = " this" // must not be a valid identifier + +type CompilerError struct { + Message string + File *file.File + Offset int +} + +type CompilerSyntaxError struct { + CompilerError +} + +type CompilerReferenceError struct { + CompilerError +} + +type srcMapItem struct { + pc int + srcPos int +} + +// Program is an internal, compiled representation of code which is produced by the Compile function. +// This representation is not linked to a runtime in any way and can be used concurrently. +// It is always preferable to use a Program over a string when running code as it skips the compilation step. +type Program struct { + code []instruction + + funcName unistring.String + src *file.File + srcMap []srcMapItem +} + +type compiler struct { + p *Program + scope *scope + block *block + + classScope *classScope + + enumGetExpr compiledEnumGetExpr + + evalVM *vm // VM used to evaluate constant expressions + ctxVM *vm // VM in which an eval() code is compiled + + codeScratchpad []instruction + + stringCache map[unistring.String]Value +} + +type binding struct { + scope *scope + name unistring.String + accessPoints map[*scope]*[]int + isConst bool + isStrict bool + isArg bool + isVar bool + inStash bool +} + +func (b *binding) getAccessPointsForScope(s *scope) *[]int { + m := b.accessPoints[s] + if m == nil { + a := make([]int, 0, 1) + m = &a + if b.accessPoints == nil { + b.accessPoints = make(map[*scope]*[]int) + } + b.accessPoints[s] = m + } + return m +} + +func (b *binding) markAccessPointAt(pos int) { + scope := b.scope.c.scope + m := b.getAccessPointsForScope(scope) + *m = append(*m, pos-scope.base) +} + +func (b *binding) markAccessPointAtScope(scope *scope, pos int) { + m := b.getAccessPointsForScope(scope) + *m = append(*m, pos-scope.base) +} + +func (b *binding) markAccessPoint() { + scope := b.scope.c.scope + m := b.getAccessPointsForScope(scope) + *m = append(*m, len(scope.prg.code)-scope.base) +} + +func (b *binding) emitGet() { + b.markAccessPoint() + if b.isVar && !b.isArg { + b.scope.c.emit(loadStack(0)) + } else { + b.scope.c.emit(loadStackLex(0)) + } +} + +func (b *binding) emitGetAt(pos int) { + b.markAccessPointAt(pos) + if b.isVar && !b.isArg { + b.scope.c.p.code[pos] = loadStack(0) + } else { + b.scope.c.p.code[pos] = loadStackLex(0) + } +} + +func (b *binding) emitGetP() { + if b.isVar && !b.isArg { + // no-op + } else { + // make sure TDZ is checked + b.markAccessPoint() + b.scope.c.emit(loadStackLex(0), pop) + } +} + +func (b *binding) emitSet() { + if b.isConst { + if b.isStrict || b.scope.c.scope.strict { + b.scope.c.emit(throwAssignToConst) + } + return + } + b.markAccessPoint() + if b.isVar && !b.isArg { + b.scope.c.emit(storeStack(0)) + } else { + b.scope.c.emit(storeStackLex(0)) + } +} + +func (b *binding) emitSetP() { + if b.isConst { + if b.isStrict || b.scope.c.scope.strict { + b.scope.c.emit(throwAssignToConst) + } + return + } + b.markAccessPoint() + if b.isVar && !b.isArg { + b.scope.c.emit(storeStackP(0)) + } else { + b.scope.c.emit(storeStackLexP(0)) + } +} + +func (b *binding) emitInitP() { + if !b.isVar && b.scope.outer == nil { + b.scope.c.emit(initGlobalP(b.name)) + } else { + b.markAccessPoint() + b.scope.c.emit(initStackP(0)) + } +} + +func (b *binding) emitInit() { + if !b.isVar && b.scope.outer == nil { + b.scope.c.emit(initGlobal(b.name)) + } else { + b.markAccessPoint() + b.scope.c.emit(initStack(0)) + } +} + +func (b *binding) emitInitAt(pos int) { + if !b.isVar && b.scope.outer == nil { + b.scope.c.p.code[pos] = initGlobal(b.name) + } else { + b.markAccessPointAt(pos) + b.scope.c.p.code[pos] = initStack(0) + } +} + +func (b *binding) emitInitAtScope(scope *scope, pos int) { + if !b.isVar && scope.outer == nil { + scope.c.p.code[pos] = initGlobal(b.name) + } else { + b.markAccessPointAtScope(scope, pos) + scope.c.p.code[pos] = initStack(0) + } +} + +func (b *binding) emitInitPAtScope(scope *scope, pos int) { + if !b.isVar && scope.outer == nil { + scope.c.p.code[pos] = initGlobalP(b.name) + } else { + b.markAccessPointAtScope(scope, pos) + scope.c.p.code[pos] = initStackP(0) + } +} + +func (b *binding) emitGetVar(callee bool) { + b.markAccessPoint() + if b.isVar && !b.isArg { + b.scope.c.emit(&loadMixed{name: b.name, callee: callee}) + } else { + b.scope.c.emit(&loadMixedLex{name: b.name, callee: callee}) + } +} + +func (b *binding) emitResolveVar(strict bool) { + b.markAccessPoint() + if b.isVar && !b.isArg { + b.scope.c.emit(&resolveMixed{name: b.name, strict: strict, typ: varTypeVar}) + } else { + var typ varType + if b.isConst { + if b.isStrict { + typ = varTypeStrictConst + } else { + typ = varTypeConst + } + } else { + typ = varTypeLet + } + b.scope.c.emit(&resolveMixed{name: b.name, strict: strict, typ: typ}) + } +} + +func (b *binding) moveToStash() { + if b.isArg && !b.scope.argsInStash { + b.scope.moveArgsToStash() + } else { + b.inStash = true + b.scope.needStash = true + } +} + +func (b *binding) useCount() (count int) { + for _, a := range b.accessPoints { + count += len(*a) + } + return +} + +type scope struct { + c *compiler + prg *Program + outer *scope + nested []*scope + boundNames map[unistring.String]*binding + bindings []*binding + base int + numArgs int + + // function type. If not funcNone, this is a function or a top-level lexical environment + funcType funcType + + // in strict mode + strict bool + // eval top-level scope + eval bool + // at least one inner scope has direct eval() which can lookup names dynamically (by name) + dynLookup bool + // at least one binding has been marked for placement in stash + needStash bool + + // is a variable environment, i.e. the target for dynamically created var bindings + variable bool + // a function scope that has at least one direct eval() and non-strict, so the variables can be added dynamically + dynamic bool + // arguments have been marked for placement in stash (functions only) + argsInStash bool + // need 'arguments' object (functions only) + argsNeeded bool +} + +type block struct { + typ blockType + label unistring.String + cont int + breaks []int + conts []int + outer *block + breaking *block // set when the 'finally' block is an empty break statement sequence + needResult bool +} + +func (c *compiler) leaveScopeBlock(enter *enterBlock) { + c.updateEnterBlock(enter) + leave := &leaveBlock{ + stackSize: enter.stackSize, + popStash: enter.stashSize > 0, + } + c.emit(leave) + for _, pc := range c.block.breaks { + c.p.code[pc] = leave + } + c.block.breaks = nil + c.leaveBlock() +} + +func (c *compiler) leaveBlock() { + lbl := len(c.p.code) + for _, item := range c.block.breaks { + c.p.code[item] = jump(lbl - item) + } + if t := c.block.typ; t == blockLoop || t == blockLoopEnum { + for _, item := range c.block.conts { + c.p.code[item] = jump(c.block.cont - item) + } + } + c.block = c.block.outer +} + +func (e *CompilerSyntaxError) Error() string { + if e.File != nil { + return fmt.Sprintf("SyntaxError: %s at %s", e.Message, e.File.Position(e.Offset)) + } + return fmt.Sprintf("SyntaxError: %s", e.Message) +} + +func (e *CompilerReferenceError) Error() string { + return fmt.Sprintf("ReferenceError: %s", e.Message) +} + +func (c *compiler) newScope() { + strict := false + if c.scope != nil { + strict = c.scope.strict + } + c.scope = &scope{ + c: c, + prg: c.p, + outer: c.scope, + strict: strict, + } +} + +func (c *compiler) newBlockScope() { + c.newScope() + if outer := c.scope.outer; outer != nil { + outer.nested = append(outer.nested, c.scope) + } + c.scope.base = len(c.p.code) +} + +func (c *compiler) popScope() { + c.scope = c.scope.outer +} + +func (c *compiler) emitLiteralString(s String) { + key := s.string() + if c.stringCache == nil { + c.stringCache = make(map[unistring.String]Value) + } + internVal := c.stringCache[key] + if internVal == nil { + c.stringCache[key] = s + internVal = s + } + + c.emit(loadVal{internVal}) +} + +func (c *compiler) emitLiteralValue(v Value) { + if s, ok := v.(String); ok { + c.emitLiteralString(s) + return + } + + c.emit(loadVal{v}) +} + +func newCompiler() *compiler { + c := &compiler{ + p: &Program{}, + } + + c.enumGetExpr.init(c, file.Idx(0)) + + return c +} + +func (p *Program) dumpCode(logger func(format string, args ...interface{})) { + p._dumpCode("", logger) +} + +func (p *Program) _dumpCode(indent string, logger func(format string, args ...interface{})) { + dumpInitFields := func(initFields *Program) { + i := indent + ">" + logger("%s ---- init_fields:", i) + initFields._dumpCode(i, logger) + logger("%s ----", i) + } + for pc, ins := range p.code { + logger("%s %d: %T(%v)", indent, pc, ins, ins) + var prg *Program + switch f := ins.(type) { + case newFuncInstruction: + prg = f.getPrg() + case *newDerivedClass: + if f.initFields != nil { + dumpInitFields(f.initFields) + } + prg = f.ctor + case *newClass: + if f.initFields != nil { + dumpInitFields(f.initFields) + } + prg = f.ctor + case *newStaticFieldInit: + if f.initFields != nil { + dumpInitFields(f.initFields) + } + } + if prg != nil { + prg._dumpCode(indent+">", logger) + } + } +} + +func (p *Program) sourceOffset(pc int) int { + i := sort.Search(len(p.srcMap), func(idx int) bool { + return p.srcMap[idx].pc > pc + }) - 1 + if i >= 0 { + return p.srcMap[i].srcPos + } + + return 0 +} + +func (p *Program) addSrcMap(srcPos int) { + if len(p.srcMap) > 0 && p.srcMap[len(p.srcMap)-1].srcPos == srcPos { + return + } + p.srcMap = append(p.srcMap, srcMapItem{pc: len(p.code), srcPos: srcPos}) +} + +func (s *scope) lookupName(name unistring.String) (binding *binding, noDynamics bool) { + noDynamics = true + toStash := false + for curScope := s; ; curScope = curScope.outer { + if curScope.outer != nil { + if b, exists := curScope.boundNames[name]; exists { + if toStash && !b.inStash { + b.moveToStash() + } + binding = b + return + } + } else { + noDynamics = false + return + } + if curScope.dynamic { + noDynamics = false + } + if name == "arguments" && curScope.funcType != funcNone && curScope.funcType != funcArrow { + if curScope.funcType == funcClsInit { + s.c.throwSyntaxError(0, "'arguments' is not allowed in class field initializer or static initialization block") + } + curScope.argsNeeded = true + binding, _ = curScope.bindName(name) + return + } + if curScope.isFunction() { + toStash = true + } + } +} + +func (s *scope) lookupThis() (*binding, bool) { + toStash := false + for curScope := s; curScope != nil; curScope = curScope.outer { + if curScope.outer == nil { + if curScope.eval { + return nil, true + } + } + if b, exists := curScope.boundNames[thisBindingName]; exists { + if toStash && !b.inStash { + b.moveToStash() + } + return b, false + } + if curScope.isFunction() { + toStash = true + } + } + return nil, false +} + +func (s *scope) ensureBoundNamesCreated() { + if s.boundNames == nil { + s.boundNames = make(map[unistring.String]*binding) + } +} + +func (s *scope) addBinding(offset int) *binding { + if len(s.bindings) >= (1<<24)-1 { + s.c.throwSyntaxError(offset, "Too many variables") + } + b := &binding{ + scope: s, + } + s.bindings = append(s.bindings, b) + return b +} + +func (s *scope) bindNameLexical(name unistring.String, unique bool, offset int) (*binding, bool) { + if b := s.boundNames[name]; b != nil { + if unique { + s.c.throwSyntaxError(offset, "Identifier '%s' has already been declared", name) + } + return b, false + } + b := s.addBinding(offset) + b.name = name + s.ensureBoundNamesCreated() + s.boundNames[name] = b + return b, true +} + +func (s *scope) createThisBinding() *binding { + thisBinding, _ := s.bindNameLexical(thisBindingName, false, 0) + thisBinding.isVar = true // don't check on load + return thisBinding +} + +func (s *scope) bindName(name unistring.String) (*binding, bool) { + if !s.isFunction() && !s.variable && s.outer != nil { + return s.outer.bindName(name) + } + b, created := s.bindNameLexical(name, false, 0) + if created { + b.isVar = true + } + return b, created +} + +func (s *scope) bindNameShadow(name unistring.String) (*binding, bool) { + if !s.isFunction() && s.outer != nil { + return s.outer.bindNameShadow(name) + } + + _, exists := s.boundNames[name] + b := &binding{ + scope: s, + name: name, + } + s.bindings = append(s.bindings, b) + s.ensureBoundNamesCreated() + s.boundNames[name] = b + return b, !exists +} + +func (s *scope) nearestFunction() *scope { + for sc := s; sc != nil; sc = sc.outer { + if sc.isFunction() { + return sc + } + } + return nil +} + +func (s *scope) nearestThis() *scope { + for sc := s; sc != nil; sc = sc.outer { + if sc.eval || sc.isFunction() && sc.funcType != funcArrow { + return sc + } + } + return nil +} + +func (s *scope) finaliseVarAlloc(stackOffset int) (stashSize, stackSize int) { + argsInStash := false + if f := s.nearestFunction(); f != nil { + argsInStash = f.argsInStash + } + stackIdx, stashIdx := 0, 0 + allInStash := s.isDynamic() + var derivedCtor bool + if fs := s.nearestThis(); fs != nil && fs.funcType == funcDerivedCtor { + derivedCtor = true + } + for i, b := range s.bindings { + var this bool + if b.name == thisBindingName { + this = true + } + if allInStash || b.inStash { + for scope, aps := range b.accessPoints { + var level uint32 + for sc := scope; sc != nil && sc != s; sc = sc.outer { + if sc.needStash || sc.isDynamic() { + level++ + } + } + if level > 255 { + s.c.throwSyntaxError(0, "Maximum nesting level (256) exceeded") + } + idx := (level << 24) | uint32(stashIdx) + base := scope.base + code := scope.prg.code + if this { + if derivedCtor { + for _, pc := range *aps { + ap := &code[base+pc] + switch (*ap).(type) { + case loadStack: + *ap = loadThisStash(idx) + case initStack: + *ap = initStash(idx) + case resolveThisStack: + *ap = resolveThisStash(idx) + case _ret: + *ap = cret(idx) + default: + s.c.assert(false, s.c.p.sourceOffset(pc), "Unsupported instruction for 'this'") + } + } + } else { + for _, pc := range *aps { + ap := &code[base+pc] + switch (*ap).(type) { + case loadStack: + *ap = loadStash(idx) + case initStack: + *ap = initStash(idx) + default: + s.c.assert(false, s.c.p.sourceOffset(pc), "Unsupported instruction for 'this'") + } + } + } + } else { + for _, pc := range *aps { + ap := &code[base+pc] + switch i := (*ap).(type) { + case loadStack: + *ap = loadStash(idx) + case storeStack: + *ap = storeStash(idx) + case storeStackP: + *ap = storeStashP(idx) + case loadStackLex: + *ap = loadStashLex(idx) + case storeStackLex: + *ap = storeStashLex(idx) + case storeStackLexP: + *ap = storeStashLexP(idx) + case initStackP: + *ap = initStashP(idx) + case initStack: + *ap = initStash(idx) + case *loadMixed: + i.idx = idx + case *loadMixedLex: + i.idx = idx + case *resolveMixed: + i.idx = idx + default: + s.c.assert(false, s.c.p.sourceOffset(pc), "Unsupported instruction for binding: %T", i) + } + } + } + } + stashIdx++ + } else { + var idx int + if !this { + if i < s.numArgs { + idx = -(i + 1) + } else { + stackIdx++ + idx = stackIdx + stackOffset + } + } + for scope, aps := range b.accessPoints { + var level int + for sc := scope; sc != nil && sc != s; sc = sc.outer { + if sc.needStash || sc.isDynamic() { + level++ + } + } + if level > 255 { + s.c.throwSyntaxError(0, "Maximum nesting level (256) exceeded") + } + code := scope.prg.code + base := scope.base + if this { + if derivedCtor { + for _, pc := range *aps { + ap := &code[base+pc] + switch (*ap).(type) { + case loadStack: + *ap = loadThisStack{} + case initStack: + // no-op + case resolveThisStack: + // no-op + case _ret: + // no-op, already in the right place + default: + s.c.assert(false, s.c.p.sourceOffset(pc), "Unsupported instruction for 'this'") + } + } + } /*else { + no-op + }*/ + } else if argsInStash { + for _, pc := range *aps { + ap := &code[base+pc] + switch i := (*ap).(type) { + case loadStack: + *ap = loadStack1(idx) + case storeStack: + *ap = storeStack1(idx) + case storeStackP: + *ap = storeStack1P(idx) + case loadStackLex: + *ap = loadStack1Lex(idx) + case storeStackLex: + *ap = storeStack1Lex(idx) + case storeStackLexP: + *ap = storeStack1LexP(idx) + case initStackP: + *ap = initStack1P(idx) + case initStack: + *ap = initStack1(idx) + case *loadMixed: + *ap = &loadMixedStack1{name: i.name, idx: idx, level: uint8(level), callee: i.callee} + case *loadMixedLex: + *ap = &loadMixedStack1Lex{name: i.name, idx: idx, level: uint8(level), callee: i.callee} + case *resolveMixed: + *ap = &resolveMixedStack1{typ: i.typ, name: i.name, idx: idx, level: uint8(level), strict: i.strict} + default: + s.c.assert(false, s.c.p.sourceOffset(pc), "Unsupported instruction for binding: %T", i) + } + } + } else { + for _, pc := range *aps { + ap := &code[base+pc] + switch i := (*ap).(type) { + case loadStack: + *ap = loadStack(idx) + case storeStack: + *ap = storeStack(idx) + case storeStackP: + *ap = storeStackP(idx) + case loadStackLex: + *ap = loadStackLex(idx) + case storeStackLex: + *ap = storeStackLex(idx) + case storeStackLexP: + *ap = storeStackLexP(idx) + case initStack: + *ap = initStack(idx) + case initStackP: + *ap = initStackP(idx) + case *loadMixed: + *ap = &loadMixedStack{name: i.name, idx: idx, level: uint8(level), callee: i.callee} + case *loadMixedLex: + *ap = &loadMixedStackLex{name: i.name, idx: idx, level: uint8(level), callee: i.callee} + case *resolveMixed: + *ap = &resolveMixedStack{typ: i.typ, name: i.name, idx: idx, level: uint8(level), strict: i.strict} + default: + s.c.assert(false, s.c.p.sourceOffset(pc), "Unsupported instruction for binding: %T", i) + } + } + } + } + } + } + for _, nested := range s.nested { + nested.finaliseVarAlloc(stackIdx + stackOffset) + } + return stashIdx, stackIdx +} + +func (s *scope) moveArgsToStash() { + for _, b := range s.bindings { + if !b.isArg { + break + } + b.inStash = true + } + s.argsInStash = true + s.needStash = true +} + +func (c *compiler) trimCode(delta int) { + src := c.p.code[delta:] + newCode := make([]instruction, len(src)) + copy(newCode, src) + if cap(c.codeScratchpad) < cap(c.p.code) { + c.codeScratchpad = c.p.code[:0] + } + c.p.code = newCode +} + +func (s *scope) trimCode(delta int) { + s.c.trimCode(delta) + if delta != 0 { + srcMap := s.c.p.srcMap + for i := range srcMap { + srcMap[i].pc -= delta + } + s.adjustBase(-delta) + } +} + +func (s *scope) adjustBase(delta int) { + s.base += delta + for _, nested := range s.nested { + nested.adjustBase(delta) + } +} + +func (s *scope) makeNamesMap() map[unistring.String]uint32 { + l := len(s.bindings) + if l == 0 { + return nil + } + names := make(map[unistring.String]uint32, l) + for i, b := range s.bindings { + idx := uint32(i) + if b.isConst { + idx |= maskConst + if b.isStrict { + idx |= maskStrict + } + } + if b.isVar { + idx |= maskVar + } + names[b.name] = idx + } + return names +} + +func (s *scope) isDynamic() bool { + return s.dynLookup || s.dynamic +} + +func (s *scope) isFunction() bool { + return s.funcType != funcNone && !s.eval +} + +func (s *scope) deleteBinding(b *binding) { + idx := 0 + for i, bb := range s.bindings { + if bb == b { + idx = i + goto found + } + } + return +found: + delete(s.boundNames, b.name) + copy(s.bindings[idx:], s.bindings[idx+1:]) + l := len(s.bindings) - 1 + s.bindings[l] = nil + s.bindings = s.bindings[:l] +} + +func (c *compiler) compile(in *ast.Program, strict, inGlobal bool, evalVm *vm) { + c.ctxVM = evalVm + + eval := evalVm != nil + c.p.src = in.File + c.newScope() + scope := c.scope + scope.dynamic = true + scope.eval = eval + if !strict && len(in.Body) > 0 { + strict = c.isStrict(in.Body) != nil + } + scope.strict = strict + ownVarScope := eval && strict + ownLexScope := !inGlobal || eval + if ownVarScope { + c.newBlockScope() + scope = c.scope + scope.variable = true + } + if eval && !inGlobal { + for s := evalVm.stash; s != nil; s = s.outer { + if ft := s.funcType; ft != funcNone && ft != funcArrow { + scope.funcType = ft + break + } + } + } + funcs := c.extractFunctions(in.Body) + c.createFunctionBindings(funcs) + numFuncs := len(scope.bindings) + if inGlobal && !ownVarScope { + if numFuncs == len(funcs) { + c.compileFunctionsGlobalAllUnique(funcs) + } else { + c.compileFunctionsGlobal(funcs) + } + } + c.compileDeclList(in.DeclarationList, false) + numVars := len(scope.bindings) - numFuncs + vars := make([]unistring.String, len(scope.bindings)) + for i, b := range scope.bindings { + vars[i] = b.name + } + if len(vars) > 0 && !ownVarScope && ownLexScope { + if inGlobal { + c.emit(&bindGlobal{ + vars: vars[numFuncs:], + funcs: vars[:numFuncs], + deletable: eval, + }) + } else { + c.emit(&bindVars{names: vars, deletable: eval}) + } + } + var enter *enterBlock + if c.compileLexicalDeclarations(in.Body, ownVarScope || !ownLexScope) { + if ownLexScope { + c.block = &block{ + outer: c.block, + typ: blockScope, + needResult: true, + } + enter = &enterBlock{} + c.emit(enter) + } + } + if len(scope.bindings) > 0 && !ownLexScope { + var lets, consts []unistring.String + for _, b := range c.scope.bindings[numFuncs+numVars:] { + if b.isConst { + consts = append(consts, b.name) + } else { + lets = append(lets, b.name) + } + } + c.emit(&bindGlobal{ + vars: vars[numFuncs:], + funcs: vars[:numFuncs], + lets: lets, + consts: consts, + }) + } + if !inGlobal || ownVarScope { + c.compileFunctions(funcs) + } + c.compileStatements(in.Body, true) + if enter != nil { + c.leaveScopeBlock(enter) + c.popScope() + } + + scope.finaliseVarAlloc(0) + c.stringCache = nil +} + +func (c *compiler) compileDeclList(v []*ast.VariableDeclaration, inFunc bool) { + for _, value := range v { + c.createVarBindings(value, inFunc) + } +} + +func (c *compiler) extractLabelled(st ast.Statement) ast.Statement { + if st, ok := st.(*ast.LabelledStatement); ok { + return c.extractLabelled(st.Statement) + } + return st +} + +func (c *compiler) extractFunctions(list []ast.Statement) (funcs []*ast.FunctionDeclaration) { + for _, st := range list { + var decl *ast.FunctionDeclaration + switch st := c.extractLabelled(st).(type) { + case *ast.FunctionDeclaration: + decl = st + case *ast.LabelledStatement: + if st1, ok := st.Statement.(*ast.FunctionDeclaration); ok { + decl = st1 + } else { + continue + } + default: + continue + } + funcs = append(funcs, decl) + } + return +} + +func (c *compiler) createFunctionBindings(funcs []*ast.FunctionDeclaration) { + s := c.scope + if s.outer != nil { + unique := !s.isFunction() && !s.variable && s.strict + if !unique { + hasNonStandard := false + for _, decl := range funcs { + if !decl.Function.Async && !decl.Function.Generator { + s.bindNameLexical(decl.Function.Name.Name, false, int(decl.Function.Name.Idx1())-1) + } else { + hasNonStandard = true + } + } + if hasNonStandard { + for _, decl := range funcs { + if decl.Function.Async || decl.Function.Generator { + s.bindNameLexical(decl.Function.Name.Name, true, int(decl.Function.Name.Idx1())-1) + } + } + } + } else { + for _, decl := range funcs { + s.bindNameLexical(decl.Function.Name.Name, true, int(decl.Function.Name.Idx1())-1) + } + } + } else { + for _, decl := range funcs { + s.bindName(decl.Function.Name.Name) + } + } +} + +func (c *compiler) compileFunctions(list []*ast.FunctionDeclaration) { + for _, decl := range list { + c.compileFunction(decl) + } +} + +func (c *compiler) compileFunctionsGlobalAllUnique(list []*ast.FunctionDeclaration) { + for _, decl := range list { + c.compileFunctionLiteral(decl.Function, false).emitGetter(true) + } +} + +func (c *compiler) compileFunctionsGlobal(list []*ast.FunctionDeclaration) { + m := make(map[unistring.String]int, len(list)) + for i := len(list) - 1; i >= 0; i-- { + name := list[i].Function.Name.Name + if _, exists := m[name]; !exists { + m[name] = i + } + } + idx := 0 + for i, decl := range list { + name := decl.Function.Name.Name + if m[name] == i { + c.compileFunctionLiteral(decl.Function, false).emitGetter(true) + c.scope.bindings[idx] = c.scope.boundNames[name] + idx++ + } else { + leave := c.enterDummyMode() + c.compileFunctionLiteral(decl.Function, false).emitGetter(false) + leave() + } + } +} + +func (c *compiler) createVarIdBinding(name unistring.String, offset int, inFunc bool) { + if c.scope.strict { + c.checkIdentifierLName(name, offset) + c.checkIdentifierName(name, offset) + } + if !inFunc || name != "arguments" { + c.scope.bindName(name) + } +} + +func (c *compiler) createBindings(target ast.Expression, createIdBinding func(name unistring.String, offset int)) { + switch target := target.(type) { + case *ast.Identifier: + createIdBinding(target.Name, int(target.Idx)-1) + case *ast.ObjectPattern: + for _, prop := range target.Properties { + switch prop := prop.(type) { + case *ast.PropertyShort: + createIdBinding(prop.Name.Name, int(prop.Name.Idx)-1) + case *ast.PropertyKeyed: + c.createBindings(prop.Value, createIdBinding) + default: + c.throwSyntaxError(int(target.Idx0()-1), "unsupported property type in ObjectPattern: %T", prop) + } + } + if target.Rest != nil { + c.createBindings(target.Rest, createIdBinding) + } + case *ast.ArrayPattern: + for _, elt := range target.Elements { + if elt != nil { + c.createBindings(elt, createIdBinding) + } + } + if target.Rest != nil { + c.createBindings(target.Rest, createIdBinding) + } + case *ast.AssignExpression: + c.createBindings(target.Left, createIdBinding) + default: + c.throwSyntaxError(int(target.Idx0()-1), "unsupported binding target: %T", target) + } +} + +func (c *compiler) createVarBinding(target ast.Expression, inFunc bool) { + c.createBindings(target, func(name unistring.String, offset int) { + c.createVarIdBinding(name, offset, inFunc) + }) +} + +func (c *compiler) createVarBindings(v *ast.VariableDeclaration, inFunc bool) { + for _, item := range v.List { + c.createVarBinding(item.Target, inFunc) + } +} + +func (c *compiler) createLexicalIdBinding(name unistring.String, isConst bool, offset int) *binding { + if name == "let" { + c.throwSyntaxError(offset, "let is disallowed as a lexically bound name") + } + if c.scope.strict { + c.checkIdentifierLName(name, offset) + c.checkIdentifierName(name, offset) + } + b, _ := c.scope.bindNameLexical(name, true, offset) + if isConst { + b.isConst, b.isStrict = true, true + } + return b +} + +func (c *compiler) createLexicalIdBindingFuncBody(name unistring.String, isConst bool, offset int, calleeBinding *binding) *binding { + if name == "let" { + c.throwSyntaxError(offset, "let is disallowed as a lexically bound name") + } + if c.scope.strict { + c.checkIdentifierLName(name, offset) + c.checkIdentifierName(name, offset) + } + paramScope := c.scope.outer + parentBinding := paramScope.boundNames[name] + if parentBinding != nil { + if parentBinding != calleeBinding && (name != "arguments" || !paramScope.argsNeeded) { + c.throwSyntaxError(offset, "Identifier '%s' has already been declared", name) + } + } + b, _ := c.scope.bindNameLexical(name, true, offset) + if isConst { + b.isConst, b.isStrict = true, true + } + return b +} + +func (c *compiler) createLexicalBinding(target ast.Expression, isConst bool) { + c.createBindings(target, func(name unistring.String, offset int) { + c.createLexicalIdBinding(name, isConst, offset) + }) +} + +func (c *compiler) createLexicalBindings(lex *ast.LexicalDeclaration) { + for _, d := range lex.List { + c.createLexicalBinding(d.Target, lex.Token == token.CONST) + } +} + +func (c *compiler) compileLexicalDeclarations(list []ast.Statement, scopeDeclared bool) bool { + for _, st := range list { + if lex, ok := st.(*ast.LexicalDeclaration); ok { + if !scopeDeclared { + c.newBlockScope() + scopeDeclared = true + } + c.createLexicalBindings(lex) + } else if cls, ok := st.(*ast.ClassDeclaration); ok { + if !scopeDeclared { + c.newBlockScope() + scopeDeclared = true + } + c.createLexicalIdBinding(cls.Class.Name.Name, false, int(cls.Class.Name.Idx)-1) + } + } + return scopeDeclared +} + +func (c *compiler) compileLexicalDeclarationsFuncBody(list []ast.Statement, calleeBinding *binding) { + for _, st := range list { + if lex, ok := st.(*ast.LexicalDeclaration); ok { + isConst := lex.Token == token.CONST + for _, d := range lex.List { + c.createBindings(d.Target, func(name unistring.String, offset int) { + c.createLexicalIdBindingFuncBody(name, isConst, offset, calleeBinding) + }) + } + } else if cls, ok := st.(*ast.ClassDeclaration); ok { + c.createLexicalIdBindingFuncBody(cls.Class.Name.Name, false, int(cls.Class.Name.Idx)-1, calleeBinding) + } + } +} + +func (c *compiler) compileFunction(v *ast.FunctionDeclaration) { + name := v.Function.Name.Name + b := c.scope.boundNames[name] + if b == nil || b.isVar { + e := &compiledIdentifierExpr{ + name: v.Function.Name.Name, + } + e.init(c, v.Function.Idx0()) + e.emitSetter(c.compileFunctionLiteral(v.Function, false), false) + } else { + c.compileFunctionLiteral(v.Function, false).emitGetter(true) + b.emitInitP() + } +} + +func (c *compiler) compileStandaloneFunctionDecl(v *ast.FunctionDeclaration) { + if v.Function.Async { + c.throwSyntaxError(int(v.Idx0())-1, "Async functions can only be declared at top level or inside a block.") + } + if v.Function.Generator { + c.throwSyntaxError(int(v.Idx0())-1, "Generators can only be declared at top level or inside a block.") + } + if c.scope.strict { + c.throwSyntaxError(int(v.Idx0())-1, "In strict mode code, functions can only be declared at top level or inside a block.") + } + c.throwSyntaxError(int(v.Idx0())-1, "In non-strict mode code, functions can only be declared at top level, inside a block, or as the body of an if statement.") +} + +func (c *compiler) emit(instructions ...instruction) { + c.p.code = append(c.p.code, instructions...) +} + +func (c *compiler) throwSyntaxError(offset int, format string, args ...interface{}) { + panic(&CompilerSyntaxError{ + CompilerError: CompilerError{ + File: c.p.src, + Offset: offset, + Message: fmt.Sprintf(format, args...), + }, + }) +} + +func (c *compiler) isStrict(list []ast.Statement) *ast.StringLiteral { + for _, st := range list { + if st, ok := st.(*ast.ExpressionStatement); ok { + if e, ok := st.Expression.(*ast.StringLiteral); ok { + if e.Literal == `"use strict"` || e.Literal == `'use strict'` { + return e + } + } else { + break + } + } else { + break + } + } + return nil +} + +func (c *compiler) isStrictStatement(s ast.Statement) *ast.StringLiteral { + if s, ok := s.(*ast.BlockStatement); ok { + return c.isStrict(s.List) + } + return nil +} + +func (c *compiler) checkIdentifierName(name unistring.String, offset int) { + switch name { + case "implements", "interface", "let", "package", "private", "protected", "public", "static", "yield": + c.throwSyntaxError(offset, "Unexpected strict mode reserved word") + } +} + +func (c *compiler) checkIdentifierLName(name unistring.String, offset int) { + switch name { + case "eval", "arguments": + c.throwSyntaxError(offset, "Assignment to eval or arguments is not allowed in strict mode") + } +} + +// Enter a 'dummy' compilation mode. Any code produced after this method is called will be discarded after +// leaveFunc is called with no additional side effects. This is useful for compiling code inside a +// constant falsy condition 'if' branch or a loop (i.e 'if (false) { ... } or while (false) { ... }). +// Such code should not be included in the final compilation result as it's never called, but it must +// still produce compilation errors if there are any. +// TODO: make sure variable lookups do not de-optimise parent scopes +func (c *compiler) enterDummyMode() (leaveFunc func()) { + savedBlock, savedProgram := c.block, c.p + if savedBlock != nil { + c.block = &block{ + typ: savedBlock.typ, + label: savedBlock.label, + outer: savedBlock.outer, + breaking: savedBlock.breaking, + } + } + c.p = &Program{ + src: c.p.src, + } + c.newScope() + return func() { + c.block, c.p = savedBlock, savedProgram + c.popScope() + } +} + +func (c *compiler) compileStatementDummy(statement ast.Statement) { + leave := c.enterDummyMode() + c.compileStatement(statement, false) + leave() +} + +func (c *compiler) assert(cond bool, offset int, msg string, args ...interface{}) { + if !cond { + c.throwSyntaxError(offset, "Compiler bug: "+msg, args...) + } +} + +func privateIdString(desc unistring.String) unistring.String { + return asciiString("#").Concat(stringValueFromRaw(desc)).string() +} + +type privateName struct { + idx int + isStatic bool + isMethod bool + hasGetter, hasSetter bool +} + +type resolvedPrivateName struct { + name unistring.String + idx uint32 + level uint8 + isStatic bool + isMethod bool +} + +func (r *resolvedPrivateName) string() unistring.String { + return privateIdString(r.name) +} + +type privateEnvRegistry struct { + fields, methods []unistring.String +} + +type classScope struct { + c *compiler + privateNames map[unistring.String]*privateName + + instanceEnv, staticEnv privateEnvRegistry + + outer *classScope +} + +func (r *privateEnvRegistry) createPrivateMethodId(name unistring.String) int { + r.methods = append(r.methods, name) + return len(r.methods) - 1 +} + +func (r *privateEnvRegistry) createPrivateFieldId(name unistring.String) int { + r.fields = append(r.fields, name) + return len(r.fields) - 1 +} + +func (s *classScope) declarePrivateId(name unistring.String, kind ast.PropertyKind, isStatic bool, offset int) { + pn := s.privateNames[name] + if pn != nil { + if pn.isStatic == isStatic { + switch kind { + case ast.PropertyKindGet: + if pn.hasSetter && !pn.hasGetter { + pn.hasGetter = true + return + } + case ast.PropertyKindSet: + if pn.hasGetter && !pn.hasSetter { + pn.hasSetter = true + return + } + } + } + s.c.throwSyntaxError(offset, "Identifier '#%s' has already been declared", name) + panic("unreachable") + } + var env *privateEnvRegistry + if isStatic { + env = &s.staticEnv + } else { + env = &s.instanceEnv + } + + pn = &privateName{ + isStatic: isStatic, + hasGetter: kind == ast.PropertyKindGet, + hasSetter: kind == ast.PropertyKindSet, + } + if kind != ast.PropertyKindValue { + pn.idx = env.createPrivateMethodId(name) + pn.isMethod = true + } else { + pn.idx = env.createPrivateFieldId(name) + } + + if s.privateNames == nil { + s.privateNames = make(map[unistring.String]*privateName) + } + s.privateNames[name] = pn +} + +func (s *classScope) getDeclaredPrivateId(name unistring.String) *privateName { + if n := s.privateNames[name]; n != nil { + return n + } + s.c.assert(false, 0, "getDeclaredPrivateId() for undeclared id") + panic("unreachable") +} + +func (c *compiler) resolvePrivateName(name unistring.String, offset int) (*resolvedPrivateName, *privateId) { + level := 0 + for s := c.classScope; s != nil; s = s.outer { + if len(s.privateNames) > 0 { + if pn := s.privateNames[name]; pn != nil { + return &resolvedPrivateName{ + name: name, + idx: uint32(pn.idx), + level: uint8(level), + isStatic: pn.isStatic, + isMethod: pn.isMethod, + }, nil + } + level++ + } + } + if c.ctxVM != nil { + for s := c.ctxVM.privEnv; s != nil; s = s.outer { + if id := s.names[name]; id != nil { + return nil, id + } + } + } + c.throwSyntaxError(offset, "Private field '#%s' must be declared in an enclosing class", name) + panic("unreachable") +} diff --git a/goja/compiler_expr.go b/goja/compiler_expr.go new file mode 100644 index 0000000..5245f26 --- /dev/null +++ b/goja/compiler_expr.go @@ -0,0 +1,3613 @@ +package goja + +import ( + "math/big" + + "github.com/dop251/goja/ast" + "github.com/dop251/goja/file" + "github.com/dop251/goja/token" + "github.com/dop251/goja/unistring" +) + +type compiledExpr interface { + emitGetter(putOnStack bool) + emitSetter(valueExpr compiledExpr, putOnStack bool) + emitRef() + emitUnary(prepare, body func(), postfix, putOnStack bool) + deleteExpr() compiledExpr + constant() bool + addSrcMap() +} + +type compiledExprOrRef interface { + compiledExpr + emitGetterOrRef() +} + +type compiledCallExpr struct { + baseCompiledExpr + args []compiledExpr + callee compiledExpr + + isVariadic bool +} + +type compiledNewExpr struct { + compiledCallExpr +} + +type compiledObjectLiteral struct { + baseCompiledExpr + expr *ast.ObjectLiteral +} + +type compiledArrayLiteral struct { + baseCompiledExpr + expr *ast.ArrayLiteral +} + +type compiledRegexpLiteral struct { + baseCompiledExpr + expr *ast.RegExpLiteral +} + +type compiledLiteral struct { + baseCompiledExpr + val Value +} + +type compiledTemplateLiteral struct { + baseCompiledExpr + tag compiledExpr + elements []*ast.TemplateElement + expressions []compiledExpr +} + +type compiledAssignExpr struct { + baseCompiledExpr + left, right compiledExpr + operator token.Token +} + +type compiledObjectAssignmentPattern struct { + baseCompiledExpr + expr *ast.ObjectPattern +} + +type compiledArrayAssignmentPattern struct { + baseCompiledExpr + expr *ast.ArrayPattern +} + +type deleteGlobalExpr struct { + baseCompiledExpr + name unistring.String +} + +type deleteVarExpr struct { + baseCompiledExpr + name unistring.String +} + +type deletePropExpr struct { + baseCompiledExpr + left compiledExpr + name unistring.String +} + +type deleteElemExpr struct { + baseCompiledExpr + left, member compiledExpr +} + +type constantExpr struct { + baseCompiledExpr + val Value +} + +type baseCompiledExpr struct { + c *compiler + offset int +} + +type compiledIdentifierExpr struct { + baseCompiledExpr + name unistring.String +} + +type compiledAwaitExpression struct { + baseCompiledExpr + arg compiledExpr +} + +type compiledYieldExpression struct { + baseCompiledExpr + arg compiledExpr + delegate bool +} + +type funcType uint8 + +const ( + funcNone funcType = iota + funcRegular + funcArrow + funcMethod + funcClsInit + funcCtor + funcDerivedCtor +) + +type compiledFunctionLiteral struct { + baseCompiledExpr + name *ast.Identifier + parameterList *ast.ParameterList + body []ast.Statement + source string + declarationList []*ast.VariableDeclaration + lhsName unistring.String + strict *ast.StringLiteral + homeObjOffset uint32 + typ funcType + isExpr bool + + isAsync, isGenerator bool +} + +type compiledBracketExpr struct { + baseCompiledExpr + left, member compiledExpr +} + +type compiledThisExpr struct { + baseCompiledExpr +} + +type compiledSuperExpr struct { + baseCompiledExpr +} + +type compiledNewTarget struct { + baseCompiledExpr +} + +type compiledSequenceExpr struct { + baseCompiledExpr + sequence []compiledExpr +} + +type compiledUnaryExpr struct { + baseCompiledExpr + operand compiledExpr + operator token.Token + postfix bool +} + +type compiledConditionalExpr struct { + baseCompiledExpr + test, consequent, alternate compiledExpr +} + +type compiledLogicalOr struct { + baseCompiledExpr + left, right compiledExpr +} + +type compiledCoalesce struct { + baseCompiledExpr + left, right compiledExpr +} + +type compiledLogicalAnd struct { + baseCompiledExpr + left, right compiledExpr +} + +type compiledBinaryExpr struct { + baseCompiledExpr + left, right compiledExpr + operator token.Token +} + +type compiledEnumGetExpr struct { + baseCompiledExpr +} + +type defaultDeleteExpr struct { + baseCompiledExpr + expr compiledExpr +} + +type compiledSpreadCallArgument struct { + baseCompiledExpr + expr compiledExpr +} + +type compiledOptionalChain struct { + baseCompiledExpr + expr compiledExpr +} + +type compiledOptional struct { + baseCompiledExpr + expr compiledExpr +} + +func (e *defaultDeleteExpr) emitGetter(putOnStack bool) { + e.expr.emitGetter(false) + if putOnStack { + e.c.emitLiteralValue(valueTrue) + } +} + +func (c *compiler) compileExpression(v ast.Expression) compiledExpr { + // log.Printf("compileExpression: %T", v) + switch v := v.(type) { + case nil: + return nil + case *ast.AssignExpression: + return c.compileAssignExpression(v) + case *ast.NumberLiteral: + return c.compileNumberLiteral(v) + case *ast.StringLiteral: + return c.compileStringLiteral(v) + case *ast.TemplateLiteral: + return c.compileTemplateLiteral(v) + case *ast.BooleanLiteral: + return c.compileBooleanLiteral(v) + case *ast.NullLiteral: + r := &compiledLiteral{ + val: _null, + } + r.init(c, v.Idx0()) + return r + case *ast.Identifier: + return c.compileIdentifierExpression(v) + case *ast.CallExpression: + return c.compileCallExpression(v) + case *ast.ObjectLiteral: + return c.compileObjectLiteral(v) + case *ast.ArrayLiteral: + return c.compileArrayLiteral(v) + case *ast.RegExpLiteral: + return c.compileRegexpLiteral(v) + case *ast.BinaryExpression: + return c.compileBinaryExpression(v) + case *ast.UnaryExpression: + return c.compileUnaryExpression(v) + case *ast.ConditionalExpression: + return c.compileConditionalExpression(v) + case *ast.FunctionLiteral: + return c.compileFunctionLiteral(v, true) + case *ast.ArrowFunctionLiteral: + return c.compileArrowFunctionLiteral(v) + case *ast.ClassLiteral: + return c.compileClassLiteral(v, true) + case *ast.DotExpression: + return c.compileDotExpression(v) + case *ast.PrivateDotExpression: + return c.compilePrivateDotExpression(v) + case *ast.BracketExpression: + return c.compileBracketExpression(v) + case *ast.ThisExpression: + r := &compiledThisExpr{} + r.init(c, v.Idx0()) + return r + case *ast.SuperExpression: + c.throwSyntaxError(int(v.Idx0())-1, "'super' keyword unexpected here") + panic("unreachable") + case *ast.SequenceExpression: + return c.compileSequenceExpression(v) + case *ast.NewExpression: + return c.compileNewExpression(v) + case *ast.MetaProperty: + return c.compileMetaProperty(v) + case *ast.ObjectPattern: + return c.compileObjectAssignmentPattern(v) + case *ast.ArrayPattern: + return c.compileArrayAssignmentPattern(v) + case *ast.OptionalChain: + r := &compiledOptionalChain{ + expr: c.compileExpression(v.Expression), + } + r.init(c, v.Idx0()) + return r + case *ast.Optional: + r := &compiledOptional{ + expr: c.compileExpression(v.Expression), + } + r.init(c, v.Idx0()) + return r + case *ast.AwaitExpression: + r := &compiledAwaitExpression{ + arg: c.compileExpression(v.Argument), + } + r.init(c, v.Await) + return r + case *ast.YieldExpression: + r := &compiledYieldExpression{ + arg: c.compileExpression(v.Argument), + delegate: v.Delegate, + } + r.init(c, v.Yield) + return r + default: + c.assert(false, int(v.Idx0())-1, "Unknown expression type: %T", v) + panic("unreachable") + } +} + +func (e *baseCompiledExpr) constant() bool { + return false +} + +func (e *baseCompiledExpr) init(c *compiler, idx file.Idx) { + e.c = c + e.offset = int(idx) - 1 +} + +func (e *baseCompiledExpr) emitSetter(compiledExpr, bool) { + e.c.throwSyntaxError(e.offset, "Not a valid left-value expression") +} + +func (e *baseCompiledExpr) emitRef() { + e.c.assert(false, e.offset, "Cannot emit reference for this type of expression") +} + +func (e *baseCompiledExpr) deleteExpr() compiledExpr { + r := &constantExpr{ + val: valueTrue, + } + r.init(e.c, file.Idx(e.offset+1)) + return r +} + +func (e *baseCompiledExpr) emitUnary(func(), func(), bool, bool) { + e.c.throwSyntaxError(e.offset, "Not a valid left-value expression") +} + +func (e *baseCompiledExpr) addSrcMap() { + if e.offset >= 0 { + e.c.p.addSrcMap(e.offset) + } +} + +func (e *constantExpr) emitGetter(putOnStack bool) { + if putOnStack { + e.addSrcMap() + e.c.emitLiteralValue(e.val) + } +} + +func (e *compiledIdentifierExpr) emitGetter(putOnStack bool) { + e.addSrcMap() + if b, noDynamics := e.c.scope.lookupName(e.name); noDynamics { + e.c.assert(b != nil, e.offset, "No dynamics and not found") + if putOnStack { + b.emitGet() + } else { + b.emitGetP() + } + } else { + if b != nil { + b.emitGetVar(false) + } else { + e.c.emit(loadDynamic(e.name)) + } + if !putOnStack { + e.c.emit(pop) + } + } +} + +func (e *compiledIdentifierExpr) emitGetterOrRef() { + e.addSrcMap() + if b, noDynamics := e.c.scope.lookupName(e.name); noDynamics { + e.c.assert(b != nil, e.offset, "No dynamics and not found") + b.emitGet() + } else { + if b != nil { + b.emitGetVar(false) + } else { + e.c.emit(loadDynamicRef(e.name)) + } + } +} + +func (e *compiledIdentifierExpr) emitGetterAndCallee() { + e.addSrcMap() + if b, noDynamics := e.c.scope.lookupName(e.name); noDynamics { + e.c.assert(b != nil, e.offset, "No dynamics and not found") + e.c.emit(loadUndef) + b.emitGet() + } else { + if b != nil { + b.emitGetVar(true) + } else { + e.c.emit(loadDynamicCallee(e.name)) + } + } +} + +func (e *compiledIdentifierExpr) emitVarSetter1(putOnStack bool, emitRight func(isRef bool)) { + e.addSrcMap() + c := e.c + + if b, noDynamics := c.scope.lookupName(e.name); noDynamics { + if c.scope.strict { + c.checkIdentifierLName(e.name, e.offset) + } + emitRight(false) + if b != nil { + if putOnStack { + b.emitSet() + } else { + b.emitSetP() + } + } else { + if c.scope.strict { + c.emit(setGlobalStrict(e.name)) + } else { + c.emit(setGlobal(e.name)) + } + if !putOnStack { + c.emit(pop) + } + } + } else { + c.emitVarRef(e.name, e.offset, b) + emitRight(true) + if putOnStack { + c.emit(putValue) + } else { + c.emit(putValueP) + } + } +} + +func (e *compiledIdentifierExpr) emitVarSetter(valueExpr compiledExpr, putOnStack bool) { + e.emitVarSetter1(putOnStack, func(bool) { + e.c.emitNamedOrConst(valueExpr, e.name) + }) +} + +func (c *compiler) emitVarRef(name unistring.String, offset int, b *binding) { + if c.scope.strict { + c.checkIdentifierLName(name, offset) + } + + if b != nil { + b.emitResolveVar(c.scope.strict) + } else { + if c.scope.strict { + c.emit(resolveVar1Strict(name)) + } else { + c.emit(resolveVar1(name)) + } + } +} + +func (e *compiledIdentifierExpr) emitRef() { + b, _ := e.c.scope.lookupName(e.name) + e.c.emitVarRef(e.name, e.offset, b) +} + +func (e *compiledIdentifierExpr) emitSetter(valueExpr compiledExpr, putOnStack bool) { + e.emitVarSetter(valueExpr, putOnStack) +} + +func (e *compiledIdentifierExpr) emitUnary(prepare, body func(), postfix, putOnStack bool) { + if putOnStack { + e.emitVarSetter1(true, func(isRef bool) { + e.c.emit(loadUndef) + if isRef { + e.c.emit(getValue) + } else { + e.emitGetter(true) + } + if prepare != nil { + prepare() + } + if !postfix { + body() + } + e.c.emit(rdupN(1)) + if postfix { + body() + } + }) + e.c.emit(pop) + } else { + e.emitVarSetter1(false, func(isRef bool) { + if isRef { + e.c.emit(getValue) + } else { + e.emitGetter(true) + } + body() + }) + } +} + +func (e *compiledIdentifierExpr) deleteExpr() compiledExpr { + if e.c.scope.strict { + e.c.throwSyntaxError(e.offset, "Delete of an unqualified identifier in strict mode") + panic("Unreachable") + } + if b, noDynamics := e.c.scope.lookupName(e.name); noDynamics { + if b == nil { + r := &deleteGlobalExpr{ + name: e.name, + } + r.init(e.c, file.Idx(0)) + return r + } + } else { + if b == nil { + r := &deleteVarExpr{ + name: e.name, + } + r.init(e.c, file.Idx(e.offset+1)) + return r + } + } + r := &compiledLiteral{ + val: valueFalse, + } + r.init(e.c, file.Idx(e.offset+1)) + return r +} + +type compiledSuperDotExpr struct { + baseCompiledExpr + name unistring.String +} + +func (e *compiledSuperDotExpr) emitGetter(putOnStack bool) { + e.c.emitLoadThis() + e.c.emit(loadSuper) + e.addSrcMap() + e.c.emit(getPropRecv(e.name)) + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *compiledSuperDotExpr) emitSetter(valueExpr compiledExpr, putOnStack bool) { + e.c.emitLoadThis() + e.c.emit(loadSuper) + valueExpr.emitGetter(true) + e.addSrcMap() + if putOnStack { + if e.c.scope.strict { + e.c.emit(setPropRecvStrict(e.name)) + } else { + e.c.emit(setPropRecv(e.name)) + } + } else { + if e.c.scope.strict { + e.c.emit(setPropRecvStrictP(e.name)) + } else { + e.c.emit(setPropRecvP(e.name)) + } + } +} + +func (e *compiledSuperDotExpr) emitUnary(prepare, body func(), postfix, putOnStack bool) { + if !putOnStack { + e.c.emitLoadThis() + e.c.emit(loadSuper, dupLast(2), getPropRecv(e.name)) + body() + e.addSrcMap() + if e.c.scope.strict { + e.c.emit(setPropRecvStrictP(e.name)) + } else { + e.c.emit(setPropRecvP(e.name)) + } + } else { + if !postfix { + e.c.emitLoadThis() + e.c.emit(loadSuper, dupLast(2), getPropRecv(e.name)) + if prepare != nil { + prepare() + } + body() + e.addSrcMap() + if e.c.scope.strict { + e.c.emit(setPropRecvStrict(e.name)) + } else { + e.c.emit(setPropRecv(e.name)) + } + } else { + e.c.emit(loadUndef) + e.c.emitLoadThis() + e.c.emit(loadSuper, dupLast(2), getPropRecv(e.name)) + if prepare != nil { + prepare() + } + e.c.emit(rdupN(3)) + body() + e.addSrcMap() + if e.c.scope.strict { + e.c.emit(setPropRecvStrictP(e.name)) + } else { + e.c.emit(setPropRecvP(e.name)) + } + } + } +} + +func (e *compiledSuperDotExpr) emitRef() { + e.c.emitLoadThis() + e.c.emit(loadSuper) + if e.c.scope.strict { + e.c.emit(getPropRefRecvStrict(e.name)) + } else { + e.c.emit(getPropRefRecv(e.name)) + } +} + +func (e *compiledSuperDotExpr) deleteExpr() compiledExpr { + return e.c.superDeleteError(e.offset) +} + +type compiledDotExpr struct { + baseCompiledExpr + left compiledExpr + name unistring.String +} + +type compiledPrivateDotExpr struct { + baseCompiledExpr + left compiledExpr + name unistring.String +} + +func (c *compiler) checkSuperBase(idx file.Idx) { + if s := c.scope.nearestThis(); s != nil { + switch s.funcType { + case funcMethod, funcClsInit, funcCtor, funcDerivedCtor: + return + } + } + c.throwSyntaxError(int(idx)-1, "'super' keyword unexpected here") + panic("unreachable") +} + +func (c *compiler) compileDotExpression(v *ast.DotExpression) compiledExpr { + if sup, ok := v.Left.(*ast.SuperExpression); ok { + c.checkSuperBase(sup.Idx) + r := &compiledSuperDotExpr{ + name: v.Identifier.Name, + } + r.init(c, v.Identifier.Idx) + return r + } + + r := &compiledDotExpr{ + left: c.compileExpression(v.Left), + name: v.Identifier.Name, + } + r.init(c, v.Identifier.Idx) + return r +} + +func (c *compiler) compilePrivateDotExpression(v *ast.PrivateDotExpression) compiledExpr { + r := &compiledPrivateDotExpr{ + left: c.compileExpression(v.Left), + name: v.Identifier.Name, + } + r.init(c, v.Identifier.Idx) + return r +} + +func (e *compiledPrivateDotExpr) _emitGetter(rn *resolvedPrivateName, id *privateId) { + if rn != nil { + e.c.emit((*getPrivatePropRes)(rn)) + } else { + e.c.emit((*getPrivatePropId)(id)) + } +} + +func (e *compiledPrivateDotExpr) _emitSetter(rn *resolvedPrivateName, id *privateId) { + if rn != nil { + e.c.emit((*setPrivatePropRes)(rn)) + } else { + e.c.emit((*setPrivatePropId)(id)) + } +} + +func (e *compiledPrivateDotExpr) _emitSetterP(rn *resolvedPrivateName, id *privateId) { + if rn != nil { + e.c.emit((*setPrivatePropResP)(rn)) + } else { + e.c.emit((*setPrivatePropIdP)(id)) + } +} + +func (e *compiledPrivateDotExpr) emitGetter(putOnStack bool) { + e.left.emitGetter(true) + e.addSrcMap() + rn, id := e.c.resolvePrivateName(e.name, e.offset) + e._emitGetter(rn, id) + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *compiledPrivateDotExpr) emitSetter(v compiledExpr, putOnStack bool) { + rn, id := e.c.resolvePrivateName(e.name, e.offset) + e.left.emitGetter(true) + v.emitGetter(true) + e.addSrcMap() + if putOnStack { + e._emitSetter(rn, id) + } else { + e._emitSetterP(rn, id) + } +} + +func (e *compiledPrivateDotExpr) emitUnary(prepare, body func(), postfix, putOnStack bool) { + rn, id := e.c.resolvePrivateName(e.name, e.offset) + if !putOnStack { + e.left.emitGetter(true) + e.c.emit(dup) + e._emitGetter(rn, id) + body() + e.addSrcMap() + e._emitSetterP(rn, id) + } else { + if !postfix { + e.left.emitGetter(true) + e.c.emit(dup) + e._emitGetter(rn, id) + if prepare != nil { + prepare() + } + body() + e.addSrcMap() + e._emitSetter(rn, id) + } else { + e.c.emit(loadUndef) + e.left.emitGetter(true) + e.c.emit(dup) + e._emitGetter(rn, id) + if prepare != nil { + prepare() + } + e.c.emit(rdupN(2)) + body() + e.addSrcMap() + e._emitSetterP(rn, id) + } + } +} + +func (e *compiledPrivateDotExpr) deleteExpr() compiledExpr { + e.c.throwSyntaxError(e.offset, "Private fields can not be deleted") + panic("unreachable") +} + +func (e *compiledPrivateDotExpr) emitRef() { + e.left.emitGetter(true) + rn, id := e.c.resolvePrivateName(e.name, e.offset) + if rn != nil { + e.c.emit((*getPrivateRefRes)(rn)) + } else { + e.c.emit((*getPrivateRefId)(id)) + } +} + +type compiledSuperBracketExpr struct { + baseCompiledExpr + member compiledExpr +} + +func (e *compiledSuperBracketExpr) emitGetter(putOnStack bool) { + e.c.emitLoadThis() + e.member.emitGetter(true) + e.c.emit(loadSuper) + e.addSrcMap() + e.c.emit(getElemRecv) + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *compiledSuperBracketExpr) emitSetter(valueExpr compiledExpr, putOnStack bool) { + e.c.emitLoadThis() + e.member.emitGetter(true) + e.c.emit(loadSuper) + valueExpr.emitGetter(true) + e.addSrcMap() + if putOnStack { + if e.c.scope.strict { + e.c.emit(setElemRecvStrict) + } else { + e.c.emit(setElemRecv) + } + } else { + if e.c.scope.strict { + e.c.emit(setElemRecvStrictP) + } else { + e.c.emit(setElemRecvP) + } + } +} + +func (e *compiledSuperBracketExpr) emitUnary(prepare, body func(), postfix, putOnStack bool) { + if !putOnStack { + e.c.emitLoadThis() + e.member.emitGetter(true) + e.c.emit(loadSuper, dupLast(3), getElemRecv) + body() + e.addSrcMap() + if e.c.scope.strict { + e.c.emit(setElemRecvStrictP) + } else { + e.c.emit(setElemRecvP) + } + } else { + if !postfix { + e.c.emitLoadThis() + e.member.emitGetter(true) + e.c.emit(loadSuper, dupLast(3), getElemRecv) + if prepare != nil { + prepare() + } + body() + e.addSrcMap() + if e.c.scope.strict { + e.c.emit(setElemRecvStrict) + } else { + e.c.emit(setElemRecv) + } + } else { + e.c.emit(loadUndef) + e.c.emitLoadThis() + e.member.emitGetter(true) + e.c.emit(loadSuper, dupLast(3), getElemRecv) + if prepare != nil { + prepare() + } + e.c.emit(rdupN(4)) + body() + e.addSrcMap() + if e.c.scope.strict { + e.c.emit(setElemRecvStrictP) + } else { + e.c.emit(setElemRecvP) + } + } + } +} + +func (e *compiledSuperBracketExpr) emitRef() { + e.c.emitLoadThis() + e.member.emitGetter(true) + e.c.emit(loadSuper) + if e.c.scope.strict { + e.c.emit(getElemRefRecvStrict) + } else { + e.c.emit(getElemRefRecv) + } +} + +func (c *compiler) superDeleteError(offset int) compiledExpr { + return c.compileEmitterExpr(func() { + c.emit(throwConst{referenceError("Unsupported reference to 'super'")}) + }, file.Idx(offset+1)) +} + +func (e *compiledSuperBracketExpr) deleteExpr() compiledExpr { + return e.c.superDeleteError(e.offset) +} + +func (c *compiler) checkConstantString(expr compiledExpr) (unistring.String, bool) { + if expr.constant() { + if val, ex := c.evalConst(expr); ex == nil { + if s, ok := val.(String); ok { + return s.string(), true + } + } + } + return "", false +} + +func (c *compiler) compileBracketExpression(v *ast.BracketExpression) compiledExpr { + if sup, ok := v.Left.(*ast.SuperExpression); ok { + c.checkSuperBase(sup.Idx) + member := c.compileExpression(v.Member) + if name, ok := c.checkConstantString(member); ok { + r := &compiledSuperDotExpr{ + name: name, + } + r.init(c, v.LeftBracket) + return r + } + + r := &compiledSuperBracketExpr{ + member: member, + } + r.init(c, v.LeftBracket) + return r + } + + left := c.compileExpression(v.Left) + member := c.compileExpression(v.Member) + if name, ok := c.checkConstantString(member); ok { + r := &compiledDotExpr{ + left: left, + name: name, + } + r.init(c, v.LeftBracket) + return r + } + + r := &compiledBracketExpr{ + left: left, + member: member, + } + r.init(c, v.LeftBracket) + return r +} + +func (e *compiledDotExpr) emitGetter(putOnStack bool) { + e.left.emitGetter(true) + e.addSrcMap() + e.c.emit(getProp(e.name)) + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *compiledDotExpr) emitRef() { + e.left.emitGetter(true) + if e.c.scope.strict { + e.c.emit(getPropRefStrict(e.name)) + } else { + e.c.emit(getPropRef(e.name)) + } +} + +func (e *compiledDotExpr) emitSetter(valueExpr compiledExpr, putOnStack bool) { + e.left.emitGetter(true) + valueExpr.emitGetter(true) + e.addSrcMap() + if e.c.scope.strict { + if putOnStack { + e.c.emit(setPropStrict(e.name)) + } else { + e.c.emit(setPropStrictP(e.name)) + } + } else { + if putOnStack { + e.c.emit(setProp(e.name)) + } else { + e.c.emit(setPropP(e.name)) + } + } +} + +func (e *compiledDotExpr) emitUnary(prepare, body func(), postfix, putOnStack bool) { + if !putOnStack { + e.left.emitGetter(true) + e.c.emit(dup) + e.c.emit(getProp(e.name)) + body() + e.addSrcMap() + if e.c.scope.strict { + e.c.emit(setPropStrictP(e.name)) + } else { + e.c.emit(setPropP(e.name)) + } + } else { + if !postfix { + e.left.emitGetter(true) + e.c.emit(dup) + e.c.emit(getProp(e.name)) + if prepare != nil { + prepare() + } + body() + e.addSrcMap() + if e.c.scope.strict { + e.c.emit(setPropStrict(e.name)) + } else { + e.c.emit(setProp(e.name)) + } + } else { + e.c.emit(loadUndef) + e.left.emitGetter(true) + e.c.emit(dup) + e.c.emit(getProp(e.name)) + if prepare != nil { + prepare() + } + e.c.emit(rdupN(2)) + body() + e.addSrcMap() + if e.c.scope.strict { + e.c.emit(setPropStrictP(e.name)) + } else { + e.c.emit(setPropP(e.name)) + } + } + } +} + +func (e *compiledDotExpr) deleteExpr() compiledExpr { + r := &deletePropExpr{ + left: e.left, + name: e.name, + } + r.init(e.c, file.Idx(e.offset)+1) + return r +} + +func (e *compiledBracketExpr) emitGetter(putOnStack bool) { + e.left.emitGetter(true) + e.member.emitGetter(true) + e.addSrcMap() + e.c.emit(getElem) + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *compiledBracketExpr) emitRef() { + e.left.emitGetter(true) + e.member.emitGetter(true) + if e.c.scope.strict { + e.c.emit(getElemRefStrict) + } else { + e.c.emit(getElemRef) + } +} + +func (e *compiledBracketExpr) emitSetter(valueExpr compiledExpr, putOnStack bool) { + e.left.emitGetter(true) + e.member.emitGetter(true) + valueExpr.emitGetter(true) + e.addSrcMap() + if e.c.scope.strict { + if putOnStack { + e.c.emit(setElemStrict) + } else { + e.c.emit(setElemStrictP) + } + } else { + if putOnStack { + e.c.emit(setElem) + } else { + e.c.emit(setElemP) + } + } +} + +func (e *compiledBracketExpr) emitUnary(prepare, body func(), postfix, putOnStack bool) { + if !putOnStack { + e.left.emitGetter(true) + e.member.emitGetter(true) + e.c.emit(dupLast(2), getElem) + body() + e.addSrcMap() + if e.c.scope.strict { + e.c.emit(setElemStrict, pop) + } else { + e.c.emit(setElem, pop) + } + } else { + if !postfix { + e.left.emitGetter(true) + e.member.emitGetter(true) + e.c.emit(dupLast(2), getElem) + if prepare != nil { + prepare() + } + body() + e.addSrcMap() + if e.c.scope.strict { + e.c.emit(setElemStrict) + } else { + e.c.emit(setElem) + } + } else { + e.c.emit(loadUndef) + e.left.emitGetter(true) + e.member.emitGetter(true) + e.c.emit(dupLast(2), getElem) + if prepare != nil { + prepare() + } + e.c.emit(rdupN(3)) + body() + e.addSrcMap() + if e.c.scope.strict { + e.c.emit(setElemStrict, pop) + } else { + e.c.emit(setElem, pop) + } + } + } +} + +func (e *compiledBracketExpr) deleteExpr() compiledExpr { + r := &deleteElemExpr{ + left: e.left, + member: e.member, + } + r.init(e.c, file.Idx(e.offset)+1) + return r +} + +func (e *deleteElemExpr) emitGetter(putOnStack bool) { + e.left.emitGetter(true) + e.member.emitGetter(true) + e.addSrcMap() + if e.c.scope.strict { + e.c.emit(deleteElemStrict) + } else { + e.c.emit(deleteElem) + } + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *deletePropExpr) emitGetter(putOnStack bool) { + e.left.emitGetter(true) + e.addSrcMap() + if e.c.scope.strict { + e.c.emit(deletePropStrict(e.name)) + } else { + e.c.emit(deleteProp(e.name)) + } + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *deleteVarExpr) emitGetter(putOnStack bool) { + /*if e.c.scope.strict { + e.c.throwSyntaxError(e.offset, "Delete of an unqualified identifier in strict mode") + return + }*/ + e.c.emit(deleteVar(e.name)) + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *deleteGlobalExpr) emitGetter(putOnStack bool) { + /*if e.c.scope.strict { + e.c.throwSyntaxError(e.offset, "Delete of an unqualified identifier in strict mode") + return + }*/ + + e.c.emit(deleteGlobal(e.name)) + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *compiledAssignExpr) emitGetter(putOnStack bool) { + switch e.operator { + case token.ASSIGN: + e.left.emitSetter(e.right, putOnStack) + case token.PLUS: + e.left.emitUnary(nil, func() { + e.right.emitGetter(true) + e.c.emit(add) + }, false, putOnStack) + case token.MINUS: + e.left.emitUnary(nil, func() { + e.right.emitGetter(true) + e.c.emit(sub) + }, false, putOnStack) + case token.MULTIPLY: + e.left.emitUnary(nil, func() { + e.right.emitGetter(true) + e.c.emit(mul) + }, false, putOnStack) + case token.EXPONENT: + e.left.emitUnary(nil, func() { + e.right.emitGetter(true) + e.c.emit(exp) + }, false, putOnStack) + case token.SLASH: + e.left.emitUnary(nil, func() { + e.right.emitGetter(true) + e.c.emit(div) + }, false, putOnStack) + case token.REMAINDER: + e.left.emitUnary(nil, func() { + e.right.emitGetter(true) + e.c.emit(mod) + }, false, putOnStack) + case token.OR: + e.left.emitUnary(nil, func() { + e.right.emitGetter(true) + e.c.emit(or) + }, false, putOnStack) + case token.AND: + e.left.emitUnary(nil, func() { + e.right.emitGetter(true) + e.c.emit(and) + }, false, putOnStack) + case token.EXCLUSIVE_OR: + e.left.emitUnary(nil, func() { + e.right.emitGetter(true) + e.c.emit(xor) + }, false, putOnStack) + case token.SHIFT_LEFT: + e.left.emitUnary(nil, func() { + e.right.emitGetter(true) + e.c.emit(sal) + }, false, putOnStack) + case token.SHIFT_RIGHT: + e.left.emitUnary(nil, func() { + e.right.emitGetter(true) + e.c.emit(sar) + }, false, putOnStack) + case token.UNSIGNED_SHIFT_RIGHT: + e.left.emitUnary(nil, func() { + e.right.emitGetter(true) + e.c.emit(shr) + }, false, putOnStack) + default: + e.c.assert(false, e.offset, "Unknown assign operator: %s", e.operator.String()) + panic("unreachable") + } +} + +func (e *compiledLiteral) emitGetter(putOnStack bool) { + if putOnStack { + e.c.emitLiteralValue(e.val) + } +} + +func (e *compiledLiteral) constant() bool { + return true +} + +func (e *compiledTemplateLiteral) emitGetter(putOnStack bool) { + if e.tag == nil { + if len(e.elements) == 0 { + e.c.emitLiteralString(stringEmpty) + } else { + tail := e.elements[len(e.elements)-1].Parsed + if len(e.elements) == 1 { + e.c.emitLiteralString(stringValueFromRaw(tail)) + } else { + stringCount := 0 + if head := e.elements[0].Parsed; head != "" { + e.c.emitLiteralString(stringValueFromRaw(head)) + stringCount++ + } + e.expressions[0].emitGetter(true) + e.c.emit(_toString{}) + stringCount++ + for i := 1; i < len(e.elements)-1; i++ { + if elt := e.elements[i].Parsed; elt != "" { + e.c.emitLiteralString(stringValueFromRaw(elt)) + stringCount++ + } + e.expressions[i].emitGetter(true) + e.c.emit(_toString{}) + stringCount++ + } + if tail != "" { + e.c.emitLiteralString(stringValueFromRaw(tail)) + stringCount++ + } + e.c.emit(concatStrings(stringCount)) + } + } + } else { + cooked := make([]Value, len(e.elements)) + raw := make([]Value, len(e.elements)) + for i, elt := range e.elements { + raw[i] = &valueProperty{ + enumerable: true, + value: newStringValue(elt.Literal), + } + var cookedVal Value + if elt.Valid { + cookedVal = stringValueFromRaw(elt.Parsed) + } else { + cookedVal = _undefined + } + cooked[i] = &valueProperty{ + enumerable: true, + value: cookedVal, + } + } + e.c.emitCallee(e.tag) + e.c.emit(&getTaggedTmplObject{ + raw: raw, + cooked: cooked, + }) + for _, expr := range e.expressions { + expr.emitGetter(true) + } + e.c.emit(call(len(e.expressions) + 1)) + } + if !putOnStack { + e.c.emit(pop) + } +} + +func (c *compiler) compileParameterBindingIdentifier(name unistring.String, offset int) (*binding, bool) { + if c.scope.strict { + c.checkIdentifierName(name, offset) + c.checkIdentifierLName(name, offset) + } + return c.scope.bindNameShadow(name) +} + +func (c *compiler) compileParameterPatternIdBinding(name unistring.String, offset int) { + if _, unique := c.compileParameterBindingIdentifier(name, offset); !unique { + c.throwSyntaxError(offset, "Duplicate parameter name not allowed in this context") + } +} + +func (c *compiler) compileParameterPatternBinding(item ast.Expression) { + c.createBindings(item, c.compileParameterPatternIdBinding) +} + +func (c *compiler) newCode(length, minCap int) (buf []instruction) { + if c.codeScratchpad != nil { + buf = c.codeScratchpad + c.codeScratchpad = nil + } + if cap(buf) < minCap { + buf = make([]instruction, length, minCap) + } else { + buf = buf[:length] + } + return +} + +func (e *compiledFunctionLiteral) compile() (prg *Program, name unistring.String, length int, strict bool) { + e.c.assert(e.typ != funcNone, e.offset, "compiledFunctionLiteral.typ is not set") + + savedPrg := e.c.p + preambleLen := 8 // enter, boxThis, loadStack(0), initThis, createArgs, set, loadCallee, init + e.c.p = &Program{ + src: e.c.p.src, + code: e.c.newCode(preambleLen, 16), + srcMap: []srcMapItem{{srcPos: e.offset}}, + } + e.c.newScope() + s := e.c.scope + s.funcType = e.typ + + if e.name != nil { + name = e.name.Name + } else { + name = e.lhsName + } + + if name != "" { + e.c.p.funcName = name + } + savedBlock := e.c.block + defer func() { + e.c.block = savedBlock + }() + + e.c.block = &block{ + typ: blockScope, + } + + if !s.strict { + s.strict = e.strict != nil + } + + hasPatterns := false + hasInits := false + firstDupIdx := -1 + + if e.parameterList.Rest != nil { + hasPatterns = true // strictly speaking not, but we need to activate all the checks + } + + // First, make sure that the first bindings correspond to the formal parameters + for _, item := range e.parameterList.List { + switch tgt := item.Target.(type) { + case *ast.Identifier: + offset := int(tgt.Idx) - 1 + b, unique := e.c.compileParameterBindingIdentifier(tgt.Name, offset) + if !unique { + firstDupIdx = offset + } + b.isArg = true + case ast.Pattern: + b := s.addBinding(int(item.Idx0()) - 1) + b.isArg = true + hasPatterns = true + default: + e.c.throwSyntaxError(int(item.Idx0())-1, "Unsupported BindingElement type: %T", item) + return + } + if item.Initializer != nil { + hasInits = true + } + + if firstDupIdx >= 0 && (hasPatterns || hasInits || s.strict || e.typ == funcArrow || e.typ == funcMethod) { + e.c.throwSyntaxError(firstDupIdx, "Duplicate parameter name not allowed in this context") + return + } + + if (hasPatterns || hasInits) && e.strict != nil { + e.c.throwSyntaxError(int(e.strict.Idx)-1, "Illegal 'use strict' directive in function with non-simple parameter list") + return + } + + if !hasInits { + length++ + } + } + + var thisBinding *binding + if e.typ != funcArrow { + thisBinding = s.createThisBinding() + } + + // create pattern bindings + if hasPatterns { + for _, item := range e.parameterList.List { + switch tgt := item.Target.(type) { + case *ast.Identifier: + // we already created those in the previous loop, skipping + default: + e.c.compileParameterPatternBinding(tgt) + } + } + if rest := e.parameterList.Rest; rest != nil { + e.c.compileParameterPatternBinding(rest) + } + } + + paramsCount := len(e.parameterList.List) + + s.numArgs = paramsCount + body := e.body + funcs := e.c.extractFunctions(body) + var calleeBinding *binding + + emitArgsRestMark := -1 + firstForwardRef := -1 + enterFunc2Mark := -1 + + if hasPatterns || hasInits { + if e.isExpr && e.name != nil { + if b, created := s.bindNameLexical(e.name.Name, false, 0); created { + b.isConst = true + calleeBinding = b + } + } + for i, item := range e.parameterList.List { + if pattern, ok := item.Target.(ast.Pattern); ok { + i := i + e.c.compilePatternInitExpr(func() { + if firstForwardRef == -1 { + s.bindings[i].emitGet() + } else { + e.c.emit(loadStackLex(-i - 1)) + } + }, item.Initializer, item.Target.Idx0()).emitGetter(true) + e.c.emitPattern(pattern, func(target, init compiledExpr) { + e.c.emitPatternLexicalAssign(target, init) + }, false) + } else if item.Initializer != nil { + markGet := len(e.c.p.code) + e.c.emit(nil) + mark := len(e.c.p.code) + e.c.emit(nil) + e.c.emitExpr(e.c.compileExpression(item.Initializer), true) + if firstForwardRef == -1 && (s.isDynamic() || s.bindings[i].useCount() > 0) { + firstForwardRef = i + } + if firstForwardRef == -1 { + s.bindings[i].emitGetAt(markGet) + } else { + e.c.p.code[markGet] = loadStackLex(-i - 1) + } + s.bindings[i].emitInitP() + e.c.p.code[mark] = jdefP(len(e.c.p.code) - mark) + } else { + if firstForwardRef == -1 && s.bindings[i].useCount() > 0 { + firstForwardRef = i + } + if firstForwardRef != -1 { + e.c.emit(loadStackLex(-i - 1)) + s.bindings[i].emitInitP() + } + } + } + if rest := e.parameterList.Rest; rest != nil { + e.c.emitAssign(rest, e.c.compileEmitterExpr( + func() { + emitArgsRestMark = len(e.c.p.code) + e.c.emit(createArgsRestStack(paramsCount)) + }, rest.Idx0()), + func(target, init compiledExpr) { + e.c.emitPatternLexicalAssign(target, init) + }) + } + if firstForwardRef != -1 { + for _, b := range s.bindings { + b.inStash = true + } + s.argsInStash = true + s.needStash = true + } + + e.c.newBlockScope() + varScope := e.c.scope + varScope.variable = true + enterFunc2Mark = len(e.c.p.code) + e.c.emit(nil) + e.c.compileDeclList(e.declarationList, false) + e.c.createFunctionBindings(funcs) + e.c.compileLexicalDeclarationsFuncBody(body, calleeBinding) + for _, b := range varScope.bindings { + if b.isVar { + if parentBinding := s.boundNames[b.name]; parentBinding != nil && parentBinding != calleeBinding { + parentBinding.emitGet() + b.emitSetP() + } + } + } + } else { + // To avoid triggering variable conflict when binding from non-strict direct eval(). + // Parameters are supposed to be in a parent scope, hence no conflict. + for _, b := range s.bindings[:paramsCount] { + b.isVar = true + } + e.c.compileDeclList(e.declarationList, true) + e.c.createFunctionBindings(funcs) + e.c.compileLexicalDeclarations(body, true) + if e.isExpr && e.name != nil { + if b, created := s.bindNameLexical(e.name.Name, false, 0); created { + b.isConst = true + calleeBinding = b + } + } + if calleeBinding != nil { + e.c.emit(loadCallee) + calleeBinding.emitInitP() + } + } + + e.c.compileFunctions(funcs) + if e.isGenerator { + e.c.emit(yieldEmpty) + } + e.c.compileStatements(body, false) + + var last ast.Statement + if l := len(body); l > 0 { + last = body[l-1] + } + if _, ok := last.(*ast.ReturnStatement); !ok { + if e.typ == funcDerivedCtor { + e.c.emit(loadUndef) + thisBinding.markAccessPoint() + e.c.emit(ret) + } else { + e.c.emit(loadUndef, ret) + } + } + + delta := 0 + code := e.c.p.code + + if s.isDynamic() && !s.argsInStash { + s.moveArgsToStash() + } + + if s.argsNeeded || s.isDynamic() && e.typ != funcArrow && e.typ != funcClsInit { + if e.typ == funcClsInit { + e.c.throwSyntaxError(e.offset, "'arguments' is not allowed in class field initializer or static initialization block") + } + b, created := s.bindNameLexical("arguments", false, 0) + if created || b.isVar { + if !s.argsInStash { + s.moveArgsToStash() + } + if s.strict { + b.isConst = true + } else { + b.isVar = e.c.scope.isFunction() + } + pos := preambleLen - 2 + delta += 2 + if s.strict || hasPatterns || hasInits { + code[pos] = createArgsUnmapped(paramsCount) + } else { + code[pos] = createArgsMapped(paramsCount) + } + pos++ + b.emitInitPAtScope(s, pos) + } + } + + if calleeBinding != nil { + if !s.isDynamic() && calleeBinding.useCount() == 0 { + s.deleteBinding(calleeBinding) + calleeBinding = nil + } else { + delta++ + calleeBinding.emitInitPAtScope(s, preambleLen-delta) + delta++ + code[preambleLen-delta] = loadCallee + } + } + + if thisBinding != nil { + if !s.isDynamic() && thisBinding.useCount() == 0 { + s.deleteBinding(thisBinding) + thisBinding = nil + } else { + if thisBinding.inStash || s.isDynamic() { + delta++ + thisBinding.emitInitAtScope(s, preambleLen-delta) + } + } + } + + stashSize, stackSize := s.finaliseVarAlloc(0) + + if thisBinding != nil && thisBinding.inStash && (!s.argsInStash || stackSize > 0) { + delta++ + code[preambleLen-delta] = loadStack(0) + } // otherwise, 'this' will be at stack[sp-1], no need to load + + if !s.strict && thisBinding != nil { + delta++ + code[preambleLen-delta] = boxThis + } + delta++ + delta = preambleLen - delta + var enter instruction + if stashSize > 0 || s.argsInStash { + if firstForwardRef == -1 { + enter1 := enterFunc{ + numArgs: uint32(paramsCount), + argsToStash: s.argsInStash, + stashSize: uint32(stashSize), + stackSize: uint32(stackSize), + extensible: s.dynamic, + funcType: e.typ, + } + if s.isDynamic() { + enter1.names = s.makeNamesMap() + } + enter = &enter1 + if enterFunc2Mark != -1 { + ef2 := &enterFuncBody{ + extensible: e.c.scope.dynamic, + funcType: e.typ, + } + e.c.updateEnterBlock(&ef2.enterBlock) + e.c.p.code[enterFunc2Mark] = ef2 + } + } else { + enter1 := enterFunc1{ + stashSize: uint32(stashSize), + numArgs: uint32(paramsCount), + argsToCopy: uint32(firstForwardRef), + extensible: s.dynamic, + funcType: e.typ, + } + if s.isDynamic() { + enter1.names = s.makeNamesMap() + } + enter = &enter1 + if enterFunc2Mark != -1 { + ef2 := &enterFuncBody{ + adjustStack: true, + extensible: e.c.scope.dynamic, + funcType: e.typ, + } + e.c.updateEnterBlock(&ef2.enterBlock) + e.c.p.code[enterFunc2Mark] = ef2 + } + } + if emitArgsRestMark != -1 && s.argsInStash { + e.c.p.code[emitArgsRestMark] = createArgsRestStash + } + } else { + enter = &enterFuncStashless{ + stackSize: uint32(stackSize), + args: uint32(paramsCount), + } + if enterFunc2Mark != -1 { + ef2 := &enterFuncBody{ + extensible: e.c.scope.dynamic, + funcType: e.typ, + } + e.c.updateEnterBlock(&ef2.enterBlock) + e.c.p.code[enterFunc2Mark] = ef2 + } + } + code[delta] = enter + e.c.p.srcMap[0].pc = delta + s.trimCode(delta) + + strict = s.strict + prg = e.c.p + // e.c.p.dumpCode() + if enterFunc2Mark != -1 { + e.c.popScope() + } + e.c.popScope() + e.c.p = savedPrg + + return +} + +func (e *compiledFunctionLiteral) emitGetter(putOnStack bool) { + p, name, length, strict := e.compile() + switch e.typ { + case funcArrow: + if e.isAsync { + e.c.emit(&newAsyncArrowFunc{newArrowFunc: newArrowFunc{newFunc: newFunc{prg: p, length: length, name: name, source: e.source, strict: strict}}}) + } else { + e.c.emit(&newArrowFunc{newFunc: newFunc{prg: p, length: length, name: name, source: e.source, strict: strict}}) + } + case funcMethod, funcClsInit: + if e.isAsync { + e.c.emit(&newAsyncMethod{newMethod: newMethod{newFunc: newFunc{prg: p, length: length, name: name, source: e.source, strict: strict}, homeObjOffset: e.homeObjOffset}}) + } else { + if e.isGenerator { + e.c.emit(&newGeneratorMethod{newMethod: newMethod{newFunc: newFunc{prg: p, length: length, name: name, source: e.source, strict: strict}, homeObjOffset: e.homeObjOffset}}) + } else { + e.c.emit(&newMethod{newFunc: newFunc{prg: p, length: length, name: name, source: e.source, strict: strict}, homeObjOffset: e.homeObjOffset}) + } + } + case funcRegular: + if e.isAsync { + e.c.emit(&newAsyncFunc{newFunc: newFunc{prg: p, length: length, name: name, source: e.source, strict: strict}}) + } else { + if e.isGenerator { + e.c.emit(&newGeneratorFunc{newFunc: newFunc{prg: p, length: length, name: name, source: e.source, strict: strict}}) + } else { + e.c.emit(&newFunc{prg: p, length: length, name: name, source: e.source, strict: strict}) + } + } + default: + e.c.throwSyntaxError(e.offset, "Unsupported func type: %v", e.typ) + } + if !putOnStack { + e.c.emit(pop) + } +} + +func (c *compiler) compileFunctionLiteral(v *ast.FunctionLiteral, isExpr bool) *compiledFunctionLiteral { + strictBody := c.isStrictStatement(v.Body) + if v.Name != nil && (c.scope.strict || strictBody != nil) { + c.checkIdentifierName(v.Name.Name, int(v.Name.Idx)-1) + c.checkIdentifierLName(v.Name.Name, int(v.Name.Idx)-1) + } + if v.Async && v.Generator { + c.throwSyntaxError(int(v.Function)-1, "Async generators are not supported yet") + } + r := &compiledFunctionLiteral{ + name: v.Name, + parameterList: v.ParameterList, + body: v.Body.List, + source: v.Source, + declarationList: v.DeclarationList, + isExpr: isExpr, + typ: funcRegular, + strict: strictBody, + isAsync: v.Async, + isGenerator: v.Generator, + } + r.init(c, v.Idx0()) + return r +} + +type compiledClassLiteral struct { + baseCompiledExpr + name *ast.Identifier + superClass compiledExpr + body []ast.ClassElement + lhsName unistring.String + source string + isExpr bool +} + +func (c *compiler) processKey(expr ast.Expression) (val unistring.String, computed bool) { + keyExpr := c.compileExpression(expr) + if keyExpr.constant() { + v, ex := c.evalConst(keyExpr) + if ex == nil { + return v.string(), false + } + } + keyExpr.emitGetter(true) + computed = true + return +} + +func (e *compiledClassLiteral) processClassKey(expr ast.Expression) (privateName *privateName, key unistring.String, computed bool) { + if p, ok := expr.(*ast.PrivateIdentifier); ok { + privateName = e.c.classScope.getDeclaredPrivateId(p.Name) + key = privateIdString(p.Name) + return + } + key, computed = e.c.processKey(expr) + return +} + +type clsElement struct { + key unistring.String + privateName *privateName + initializer compiledExpr + body *compiledFunctionLiteral + computed bool +} + +func (e *compiledClassLiteral) emitGetter(putOnStack bool) { + e.c.newBlockScope() + s := e.c.scope + s.strict = true + + enter := &enterBlock{} + mark0 := len(e.c.p.code) + e.c.emit(enter) + e.c.block = &block{ + typ: blockScope, + outer: e.c.block, + } + var clsBinding *binding + var clsName unistring.String + if name := e.name; name != nil { + clsName = name.Name + clsBinding = e.c.createLexicalIdBinding(clsName, true, int(name.Idx)-1) + } else { + clsName = e.lhsName + } + + var ctorMethod *ast.MethodDefinition + ctorMethodIdx := -1 + staticsCount := 0 + instanceFieldsCount := 0 + hasStaticPrivateMethods := false + cs := &classScope{ + c: e.c, + outer: e.c.classScope, + } + + for idx, elt := range e.body { + switch elt := elt.(type) { + case *ast.ClassStaticBlock: + if len(elt.Block.List) > 0 { + staticsCount++ + } + case *ast.FieldDefinition: + if id, ok := elt.Key.(*ast.PrivateIdentifier); ok { + cs.declarePrivateId(id.Name, ast.PropertyKindValue, elt.Static, int(elt.Idx)-1) + } + if elt.Static { + staticsCount++ + } else { + instanceFieldsCount++ + } + case *ast.MethodDefinition: + if !elt.Static { + if id, ok := elt.Key.(*ast.StringLiteral); ok { + if !elt.Computed && id.Value == "constructor" { + if ctorMethod != nil { + e.c.throwSyntaxError(int(id.Idx)-1, "A class may only have one constructor") + } + ctorMethod = elt + ctorMethodIdx = idx + continue + } + } + } + if id, ok := elt.Key.(*ast.PrivateIdentifier); ok { + cs.declarePrivateId(id.Name, elt.Kind, elt.Static, int(elt.Idx)-1) + if elt.Static { + hasStaticPrivateMethods = true + } + } + default: + e.c.assert(false, int(elt.Idx0())-1, "Unsupported static element: %T", elt) + } + } + + var staticInit *newStaticFieldInit + if staticsCount > 0 || hasStaticPrivateMethods { + staticInit = &newStaticFieldInit{} + e.c.emit(staticInit) + } + + var derived bool + var newClassIns *newClass + if superClass := e.superClass; superClass != nil { + derived = true + superClass.emitGetter(true) + ndc := &newDerivedClass{ + newClass: newClass{ + name: clsName, + source: e.source, + }, + } + e.addSrcMap() + e.c.emit(ndc) + newClassIns = &ndc.newClass + } else { + newClassIns = &newClass{ + name: clsName, + source: e.source, + } + e.addSrcMap() + e.c.emit(newClassIns) + } + + e.c.classScope = cs + + if ctorMethod != nil { + newClassIns.ctor, newClassIns.length = e.c.compileCtor(ctorMethod.Body, derived) + } + + curIsPrototype := false + + instanceFields := make([]clsElement, 0, instanceFieldsCount) + staticElements := make([]clsElement, 0, staticsCount) + + // stack at this point: + // + // staticFieldInit (if staticsCount > 0 || hasStaticPrivateMethods) + // prototype + // class function + // <- sp + + for idx, elt := range e.body { + if idx == ctorMethodIdx { + continue + } + switch elt := elt.(type) { + case *ast.ClassStaticBlock: + if len(elt.Block.List) > 0 { + f := e.c.compileFunctionLiteral(&ast.FunctionLiteral{ + Function: elt.Idx0(), + ParameterList: &ast.ParameterList{}, + Body: elt.Block, + Source: elt.Source, + DeclarationList: elt.DeclarationList, + }, true) + f.typ = funcClsInit + //f.lhsName = "" + f.homeObjOffset = 1 + staticElements = append(staticElements, clsElement{ + body: f, + }) + } + case *ast.FieldDefinition: + privateName, key, computed := e.processClassKey(elt.Key) + var el clsElement + if elt.Initializer != nil { + el.initializer = e.c.compileExpression(elt.Initializer) + } + el.computed = computed + if computed { + if elt.Static { + if curIsPrototype { + e.c.emit(defineComputedKey(5)) + } else { + e.c.emit(defineComputedKey(4)) + } + } else { + if curIsPrototype { + e.c.emit(defineComputedKey(3)) + } else { + e.c.emit(defineComputedKey(2)) + } + } + } else { + el.privateName = privateName + el.key = key + } + if elt.Static { + staticElements = append(staticElements, el) + } else { + instanceFields = append(instanceFields, el) + } + case *ast.MethodDefinition: + if elt.Static { + if curIsPrototype { + e.c.emit(pop) + curIsPrototype = false + } + } else { + if !curIsPrototype { + e.c.emit(dupN(1)) + curIsPrototype = true + } + } + privateName, key, computed := e.processClassKey(elt.Key) + lit := e.c.compileFunctionLiteral(elt.Body, true) + lit.typ = funcMethod + if computed { + e.c.emit(_toPropertyKey{}) + lit.homeObjOffset = 2 + } else { + lit.homeObjOffset = 1 + lit.lhsName = key + } + lit.emitGetter(true) + if privateName != nil { + var offset int + if elt.Static { + if curIsPrototype { + /* + staticInit + proto + cls + proto + method + <- sp + */ + offset = 5 + } else { + /* + staticInit + proto + cls + method + <- sp + */ + offset = 4 + } + } else { + if curIsPrototype { + offset = 3 + } else { + offset = 2 + } + } + switch elt.Kind { + case ast.PropertyKindGet: + e.c.emit(&definePrivateGetter{ + definePrivateMethod: definePrivateMethod{ + idx: privateName.idx, + targetOffset: offset, + }, + }) + case ast.PropertyKindSet: + e.c.emit(&definePrivateSetter{ + definePrivateMethod: definePrivateMethod{ + idx: privateName.idx, + targetOffset: offset, + }, + }) + default: + e.c.emit(&definePrivateMethod{ + idx: privateName.idx, + targetOffset: offset, + }) + } + } else if computed { + switch elt.Kind { + case ast.PropertyKindGet: + e.c.emit(&defineGetter{}) + case ast.PropertyKindSet: + e.c.emit(&defineSetter{}) + default: + e.c.emit(&defineMethod{}) + } + } else { + switch elt.Kind { + case ast.PropertyKindGet: + e.c.emit(&defineGetterKeyed{key: key}) + case ast.PropertyKindSet: + e.c.emit(&defineSetterKeyed{key: key}) + default: + e.c.emit(&defineMethodKeyed{key: key}) + } + } + } + } + if curIsPrototype { + e.c.emit(pop) + } + + if len(instanceFields) > 0 { + newClassIns.initFields = e.compileFieldsAndStaticBlocks(instanceFields, "") + } + if staticInit != nil { + if len(staticElements) > 0 { + staticInit.initFields = e.compileFieldsAndStaticBlocks(staticElements, "") + } + } + + env := e.c.classScope.instanceEnv + if s.dynLookup { + newClassIns.privateMethods, newClassIns.privateFields = env.methods, env.fields + } + newClassIns.numPrivateMethods = uint32(len(env.methods)) + newClassIns.numPrivateFields = uint32(len(env.fields)) + newClassIns.hasPrivateEnv = len(e.c.classScope.privateNames) > 0 + + if (clsBinding != nil && clsBinding.useCount() > 0) || s.dynLookup { + if clsBinding != nil { + // Because this block may be in the middle of an expression, its initial stack position + // cannot be known, and therefore it may not have any stack variables. + // Note, because clsBinding would be accessed through a function, it should already be in stash, + // this is just to make sure. + clsBinding.moveToStash() + clsBinding.emitInit() + } + } else { + if clsBinding != nil { + s.deleteBinding(clsBinding) + clsBinding = nil + } + e.c.p.code[mark0] = jump(1) + } + + if staticsCount > 0 || hasStaticPrivateMethods { + ise := &initStaticElements{} + e.c.emit(ise) + env := e.c.classScope.staticEnv + staticInit.numPrivateFields = uint32(len(env.fields)) + staticInit.numPrivateMethods = uint32(len(env.methods)) + if s.dynLookup { + // These cannot be set on staticInit, because it is executed before ClassHeritage, and therefore + // the VM's PrivateEnvironment is still not set. + ise.privateFields = env.fields + ise.privateMethods = env.methods + } + } else { + e.c.emit(endVariadic) // re-using as semantics match + } + + if !putOnStack { + e.c.emit(pop) + } + + if clsBinding != nil || s.dynLookup { + e.c.leaveScopeBlock(enter) + e.c.assert(enter.stackSize == 0, e.offset, "enter.StackSize != 0 in compiledClassLiteral") + } else { + e.c.block = e.c.block.outer + } + if len(e.c.classScope.privateNames) > 0 { + e.c.emit(popPrivateEnv{}) + } + e.c.classScope = e.c.classScope.outer + e.c.popScope() +} + +func (e *compiledClassLiteral) compileFieldsAndStaticBlocks(elements []clsElement, funcName unistring.String) *Program { + savedPrg := e.c.p + savedBlock := e.c.block + defer func() { + e.c.p = savedPrg + e.c.block = savedBlock + }() + + e.c.block = &block{ + typ: blockScope, + } + + e.c.p = &Program{ + src: savedPrg.src, + funcName: funcName, + code: e.c.newCode(2, 16), + } + + e.c.newScope() + s := e.c.scope + s.funcType = funcClsInit + thisBinding := s.createThisBinding() + + valIdx := 0 + for _, elt := range elements { + if elt.body != nil { + e.c.emit(dup) // this + elt.body.emitGetter(true) + elt.body.addSrcMap() + e.c.emit(call(0), pop) + } else { + if elt.computed { + e.c.emit(loadComputedKey(valIdx)) + valIdx++ + } + if init := elt.initializer; init != nil { + if !elt.computed { + e.c.emitNamedOrConst(init, elt.key) + } else { + e.c.emitExpr(init, true) + } + } else { + e.c.emit(loadUndef) + } + if elt.privateName != nil { + e.c.emit(&definePrivateProp{ + idx: elt.privateName.idx, + }) + } else if elt.computed { + e.c.emit(defineProp{}) + } else { + e.c.emit(definePropKeyed(elt.key)) + } + } + } + //e.c.emit(halt) + if s.isDynamic() || thisBinding.useCount() > 0 { + if s.isDynamic() || thisBinding.inStash { + thisBinding.emitInitAt(1) + } + } else { + s.deleteBinding(thisBinding) + } + stashSize, stackSize := s.finaliseVarAlloc(0) + e.c.assert(stackSize == 0, e.offset, "stackSize != 0 in initFields") + if stashSize > 0 { + e.c.assert(stashSize == 1, e.offset, "stashSize != 1 in initFields") + enter := &enterFunc{ + stashSize: 1, + funcType: funcClsInit, + } + if s.dynLookup { + enter.names = s.makeNamesMap() + } + e.c.p.code[0] = enter + s.trimCode(0) + } else { + s.trimCode(2) + } + res := e.c.p + e.c.popScope() + return res +} + +func (c *compiler) compileClassLiteral(v *ast.ClassLiteral, isExpr bool) *compiledClassLiteral { + if v.Name != nil { + c.checkIdentifierLName(v.Name.Name, int(v.Name.Idx)-1) + } + r := &compiledClassLiteral{ + name: v.Name, + superClass: c.compileExpression(v.SuperClass), + body: v.Body, + source: v.Source, + isExpr: isExpr, + } + r.init(c, v.Idx0()) + return r +} + +func (c *compiler) compileCtor(ctor *ast.FunctionLiteral, derived bool) (p *Program, length int) { + f := c.compileFunctionLiteral(ctor, true) + if derived { + f.typ = funcDerivedCtor + } else { + f.typ = funcCtor + } + p, _, length, _ = f.compile() + return +} + +func (c *compiler) compileArrowFunctionLiteral(v *ast.ArrowFunctionLiteral) *compiledFunctionLiteral { + var strictBody *ast.StringLiteral + var body []ast.Statement + switch b := v.Body.(type) { + case *ast.BlockStatement: + strictBody = c.isStrictStatement(b) + body = b.List + case *ast.ExpressionBody: + body = []ast.Statement{ + &ast.ReturnStatement{ + Argument: b.Expression, + }, + } + default: + c.throwSyntaxError(int(b.Idx0())-1, "Unsupported ConciseBody type: %T", b) + } + r := &compiledFunctionLiteral{ + parameterList: v.ParameterList, + body: body, + source: v.Source, + declarationList: v.DeclarationList, + isExpr: true, + typ: funcArrow, + strict: strictBody, + isAsync: v.Async, + } + r.init(c, v.Idx0()) + return r +} + +func (c *compiler) emitLoadThis() { + b, eval := c.scope.lookupThis() + if b != nil { + b.emitGet() + } else { + if eval { + c.emit(getThisDynamic{}) + } else { + c.emit(loadGlobalObject) + } + } +} + +func (e *compiledThisExpr) emitGetter(putOnStack bool) { + e.addSrcMap() + e.c.emitLoadThis() + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *compiledSuperExpr) emitGetter(putOnStack bool) { + if putOnStack { + e.c.emit(loadSuper) + } +} + +func (e *compiledNewExpr) emitGetter(putOnStack bool) { + if e.isVariadic { + e.c.emit(startVariadic) + } + e.callee.emitGetter(true) + for _, expr := range e.args { + expr.emitGetter(true) + } + e.addSrcMap() + if e.isVariadic { + e.c.emit(newVariadic, endVariadic) + } else { + e.c.emit(_new(len(e.args))) + } + if !putOnStack { + e.c.emit(pop) + } +} + +func (c *compiler) compileCallArgs(list []ast.Expression) (args []compiledExpr, isVariadic bool) { + args = make([]compiledExpr, len(list)) + for i, argExpr := range list { + if spread, ok := argExpr.(*ast.SpreadElement); ok { + args[i] = c.compileSpreadCallArgument(spread) + isVariadic = true + } else { + args[i] = c.compileExpression(argExpr) + } + } + return +} + +func (c *compiler) compileNewExpression(v *ast.NewExpression) compiledExpr { + args, isVariadic := c.compileCallArgs(v.ArgumentList) + r := &compiledNewExpr{ + compiledCallExpr: compiledCallExpr{ + callee: c.compileExpression(v.Callee), + args: args, + isVariadic: isVariadic, + }, + } + r.init(c, v.Idx0()) + return r +} + +func (e *compiledNewTarget) emitGetter(putOnStack bool) { + if s := e.c.scope.nearestThis(); s == nil || s.funcType == funcNone { + e.c.throwSyntaxError(e.offset, "new.target expression is not allowed here") + } + if putOnStack { + e.addSrcMap() + e.c.emit(loadNewTarget) + } +} + +func (c *compiler) compileMetaProperty(v *ast.MetaProperty) compiledExpr { + if v.Meta.Name == "new" || v.Property.Name != "target" { + r := &compiledNewTarget{} + r.init(c, v.Idx0()) + return r + } + c.throwSyntaxError(int(v.Idx)-1, "Unsupported meta property: %s.%s", v.Meta.Name, v.Property.Name) + return nil +} + +func (e *compiledSequenceExpr) emitGetter(putOnStack bool) { + if len(e.sequence) > 0 { + for i := 0; i < len(e.sequence)-1; i++ { + e.sequence[i].emitGetter(false) + } + e.sequence[len(e.sequence)-1].emitGetter(putOnStack) + } +} + +func (c *compiler) compileSequenceExpression(v *ast.SequenceExpression) compiledExpr { + s := make([]compiledExpr, len(v.Sequence)) + for i, expr := range v.Sequence { + s[i] = c.compileExpression(expr) + } + r := &compiledSequenceExpr{ + sequence: s, + } + var idx file.Idx + if len(v.Sequence) > 0 { + idx = v.Idx0() + } + r.init(c, idx) + return r +} + +func (c *compiler) emitThrow(v Value) { + if o, ok := v.(*Object); ok { + t := nilSafe(o.self.getStr("name", nil)).toString().String() + switch t { + case "TypeError", "RangeError": + c.emit(loadDynamic(t)) + msg := o.self.getStr("message", nil) + if msg != nil { + c.emitLiteralValue(msg) + c.emit(_new(1)) + } else { + c.emit(_new(0)) + } + c.emit(throw) + return + } + } + c.assert(false, 0, "unknown exception type thrown while evaluating constant expression: %s", v.String()) + panic("unreachable") +} + +func (c *compiler) emitConst(expr compiledExpr, putOnStack bool) { + v, ex := c.evalConst(expr) + if ex == nil { + if putOnStack { + c.emitLiteralValue(v) + } + } else { + c.emitThrow(ex.val) + } +} + +func (c *compiler) evalConst(expr compiledExpr) (Value, *Exception) { + if expr, ok := expr.(*compiledLiteral); ok { + return expr.val, nil + } + if c.evalVM == nil { + c.evalVM = New().vm + } + var savedPrg *Program + createdPrg := false + if c.evalVM.prg == nil { + c.evalVM.prg = &Program{ + src: c.p.src, + } + savedPrg = c.p + c.p = c.evalVM.prg + createdPrg = true + } + savedPc := len(c.p.code) + expr.emitGetter(true) + c.evalVM.pc = savedPc + ex := c.evalVM.runTry() + if createdPrg { + c.evalVM.prg = nil + c.evalVM.pc = 0 + c.p = savedPrg + } else { + c.evalVM.prg.code = c.evalVM.prg.code[:savedPc] + c.p.code = c.evalVM.prg.code + } + if ex == nil { + return c.evalVM.pop(), nil + } + return nil, ex +} + +func (e *compiledUnaryExpr) constant() bool { + return e.operand.constant() +} + +func (e *compiledUnaryExpr) emitGetter(putOnStack bool) { + var prepare, body func() + + toNumber := func() { + e.addSrcMap() + e.c.emit(toNumber) + } + + switch e.operator { + case token.NOT: + e.operand.emitGetter(true) + e.c.emit(not) + goto end + case token.BITWISE_NOT: + e.operand.emitGetter(true) + e.c.emit(bnot) + goto end + case token.TYPEOF: + if o, ok := e.operand.(compiledExprOrRef); ok { + o.emitGetterOrRef() + } else { + e.operand.emitGetter(true) + } + e.c.emit(typeof) + goto end + case token.DELETE: + e.operand.deleteExpr().emitGetter(putOnStack) + return + case token.MINUS: + e.c.emitExpr(e.operand, true) + e.c.emit(neg) + goto end + case token.PLUS: + e.c.emitExpr(e.operand, true) + e.c.emit(plus) + goto end + case token.INCREMENT: + prepare = toNumber + body = func() { + e.c.emit(inc) + } + case token.DECREMENT: + prepare = toNumber + body = func() { + e.c.emit(dec) + } + case token.VOID: + e.c.emitExpr(e.operand, false) + if putOnStack { + e.c.emit(loadUndef) + } + return + default: + e.c.assert(false, e.offset, "Unknown unary operator: %s", e.operator.String()) + panic("unreachable") + } + + e.operand.emitUnary(prepare, body, e.postfix, putOnStack) + return + +end: + if !putOnStack { + e.c.emit(pop) + } +} + +func (c *compiler) compileUnaryExpression(v *ast.UnaryExpression) compiledExpr { + r := &compiledUnaryExpr{ + operand: c.compileExpression(v.Operand), + operator: v.Operator, + postfix: v.Postfix, + } + r.init(c, v.Idx0()) + return r +} + +func (e *compiledConditionalExpr) emitGetter(putOnStack bool) { + e.test.emitGetter(true) + j := len(e.c.p.code) + e.c.emit(nil) + e.consequent.emitGetter(putOnStack) + j1 := len(e.c.p.code) + e.c.emit(nil) + e.c.p.code[j] = jne(len(e.c.p.code) - j) + e.alternate.emitGetter(putOnStack) + e.c.p.code[j1] = jump(len(e.c.p.code) - j1) +} + +func (c *compiler) compileConditionalExpression(v *ast.ConditionalExpression) compiledExpr { + r := &compiledConditionalExpr{ + test: c.compileExpression(v.Test), + consequent: c.compileExpression(v.Consequent), + alternate: c.compileExpression(v.Alternate), + } + r.init(c, v.Idx0()) + return r +} + +func (e *compiledLogicalOr) constant() bool { + if e.left.constant() { + if v, ex := e.c.evalConst(e.left); ex == nil { + if v.ToBoolean() { + return true + } + return e.right.constant() + } else { + return true + } + } + + return false +} + +func (e *compiledLogicalOr) emitGetter(putOnStack bool) { + if e.left.constant() { + if v, ex := e.c.evalConst(e.left); ex == nil { + if !v.ToBoolean() { + e.c.emitExpr(e.right, putOnStack) + } else { + if putOnStack { + e.c.emitLiteralValue(v) + } + } + } else { + e.c.emitThrow(ex.val) + } + return + } + e.c.emitExpr(e.left, true) + j := len(e.c.p.code) + e.addSrcMap() + e.c.emit(nil) + e.c.emitExpr(e.right, true) + e.c.p.code[j] = jeq1(len(e.c.p.code) - j) + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *compiledCoalesce) constant() bool { + if e.left.constant() { + if v, ex := e.c.evalConst(e.left); ex == nil { + if v != _null && v != _undefined { + return true + } + return e.right.constant() + } else { + return true + } + } + + return false +} + +func (e *compiledCoalesce) emitGetter(putOnStack bool) { + if e.left.constant() { + if v, ex := e.c.evalConst(e.left); ex == nil { + if v == _undefined || v == _null { + e.c.emitExpr(e.right, putOnStack) + } else { + if putOnStack { + e.c.emitLiteralValue(v) + } + } + } else { + e.c.emitThrow(ex.val) + } + return + } + e.c.emitExpr(e.left, true) + j := len(e.c.p.code) + e.addSrcMap() + e.c.emit(nil) + e.c.emitExpr(e.right, true) + e.c.p.code[j] = jcoalesc(len(e.c.p.code) - j) + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *compiledLogicalAnd) constant() bool { + if e.left.constant() { + if v, ex := e.c.evalConst(e.left); ex == nil { + if !v.ToBoolean() { + return true + } else { + return e.right.constant() + } + } else { + return true + } + } + + return false +} + +func (e *compiledLogicalAnd) emitGetter(putOnStack bool) { + var j int + if e.left.constant() { + if v, ex := e.c.evalConst(e.left); ex == nil { + if !v.ToBoolean() { + e.c.emitLiteralValue(v) + } else { + e.c.emitExpr(e.right, putOnStack) + } + } else { + e.c.emitThrow(ex.val) + } + return + } + e.left.emitGetter(true) + j = len(e.c.p.code) + e.addSrcMap() + e.c.emit(nil) + e.c.emitExpr(e.right, true) + e.c.p.code[j] = jneq1(len(e.c.p.code) - j) + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *compiledBinaryExpr) constant() bool { + return e.left.constant() && e.right.constant() +} + +func (e *compiledBinaryExpr) emitGetter(putOnStack bool) { + e.c.emitExpr(e.left, true) + e.c.emitExpr(e.right, true) + e.addSrcMap() + + switch e.operator { + case token.LESS: + e.c.emit(op_lt) + case token.GREATER: + e.c.emit(op_gt) + case token.LESS_OR_EQUAL: + e.c.emit(op_lte) + case token.GREATER_OR_EQUAL: + e.c.emit(op_gte) + case token.EQUAL: + e.c.emit(op_eq) + case token.NOT_EQUAL: + e.c.emit(op_neq) + case token.STRICT_EQUAL: + e.c.emit(op_strict_eq) + case token.STRICT_NOT_EQUAL: + e.c.emit(op_strict_neq) + case token.PLUS: + e.c.emit(add) + case token.MINUS: + e.c.emit(sub) + case token.MULTIPLY: + e.c.emit(mul) + case token.EXPONENT: + e.c.emit(exp) + case token.SLASH: + e.c.emit(div) + case token.REMAINDER: + e.c.emit(mod) + case token.AND: + e.c.emit(and) + case token.OR: + e.c.emit(or) + case token.EXCLUSIVE_OR: + e.c.emit(xor) + case token.INSTANCEOF: + e.c.emit(op_instanceof) + case token.IN: + e.c.emit(op_in) + case token.SHIFT_LEFT: + e.c.emit(sal) + case token.SHIFT_RIGHT: + e.c.emit(sar) + case token.UNSIGNED_SHIFT_RIGHT: + e.c.emit(shr) + default: + e.c.assert(false, e.offset, "Unknown operator: %s", e.operator.String()) + panic("unreachable") + } + + if !putOnStack { + e.c.emit(pop) + } +} + +func (c *compiler) compileBinaryExpression(v *ast.BinaryExpression) compiledExpr { + + switch v.Operator { + case token.LOGICAL_OR: + return c.compileLogicalOr(v.Left, v.Right, v.Idx0()) + case token.COALESCE: + return c.compileCoalesce(v.Left, v.Right, v.Idx0()) + case token.LOGICAL_AND: + return c.compileLogicalAnd(v.Left, v.Right, v.Idx0()) + } + + if id, ok := v.Left.(*ast.PrivateIdentifier); ok { + return c.compilePrivateIn(id, v.Right, id.Idx) + } + + r := &compiledBinaryExpr{ + left: c.compileExpression(v.Left), + right: c.compileExpression(v.Right), + operator: v.Operator, + } + r.init(c, v.Idx0()) + return r +} + +type compiledPrivateIn struct { + baseCompiledExpr + id unistring.String + right compiledExpr +} + +func (e *compiledPrivateIn) emitGetter(putOnStack bool) { + e.right.emitGetter(true) + rn, id := e.c.resolvePrivateName(e.id, e.offset) + if rn != nil { + e.c.emit((*privateInRes)(rn)) + } else { + e.c.emit((*privateInId)(id)) + } + if !putOnStack { + e.c.emit(pop) + } +} + +func (c *compiler) compilePrivateIn(id *ast.PrivateIdentifier, right ast.Expression, idx file.Idx) compiledExpr { + r := &compiledPrivateIn{ + id: id.Name, + right: c.compileExpression(right), + } + r.init(c, idx) + return r +} + +func (c *compiler) compileLogicalOr(left, right ast.Expression, idx file.Idx) compiledExpr { + r := &compiledLogicalOr{ + left: c.compileExpression(left), + right: c.compileExpression(right), + } + r.init(c, idx) + return r +} + +func (c *compiler) compileCoalesce(left, right ast.Expression, idx file.Idx) compiledExpr { + r := &compiledCoalesce{ + left: c.compileExpression(left), + right: c.compileExpression(right), + } + r.init(c, idx) + return r +} + +func (c *compiler) compileLogicalAnd(left, right ast.Expression, idx file.Idx) compiledExpr { + r := &compiledLogicalAnd{ + left: c.compileExpression(left), + right: c.compileExpression(right), + } + r.init(c, idx) + return r +} + +func (e *compiledObjectLiteral) emitGetter(putOnStack bool) { + e.addSrcMap() + e.c.emit(newObject) + hasProto := false + for _, prop := range e.expr.Value { + switch prop := prop.(type) { + case *ast.PropertyKeyed: + key, computed := e.c.processKey(prop.Key) + valueExpr := e.c.compileExpression(prop.Value) + var ne namedEmitter + if fn, ok := valueExpr.(*compiledFunctionLiteral); ok { + if fn.name == nil { + ne = fn + } + switch prop.Kind { + case ast.PropertyKindMethod, ast.PropertyKindGet, ast.PropertyKindSet: + fn.typ = funcMethod + if computed { + fn.homeObjOffset = 2 + } else { + fn.homeObjOffset = 1 + } + } + } else if v, ok := valueExpr.(namedEmitter); ok { + ne = v + } + if computed { + e.c.emit(_toPropertyKey{}) + e.c.emitExpr(valueExpr, true) + switch prop.Kind { + case ast.PropertyKindValue: + if ne != nil { + e.c.emit(setElem1Named) + } else { + e.c.emit(setElem1) + } + case ast.PropertyKindMethod: + e.c.emit(&defineMethod{enumerable: true}) + case ast.PropertyKindGet: + e.c.emit(&defineGetter{enumerable: true}) + case ast.PropertyKindSet: + e.c.emit(&defineSetter{enumerable: true}) + default: + e.c.assert(false, e.offset, "unknown property kind: %s", prop.Kind) + panic("unreachable") + } + } else { + isProto := key == __proto__ && !prop.Computed + if isProto { + if hasProto { + e.c.throwSyntaxError(int(prop.Idx0())-1, "Duplicate __proto__ fields are not allowed in object literals") + } else { + hasProto = true + } + } + if ne != nil && !isProto { + ne.emitNamed(key) + } else { + e.c.emitExpr(valueExpr, true) + } + switch prop.Kind { + case ast.PropertyKindValue: + if isProto { + e.c.emit(setProto) + } else { + e.c.emit(putProp(key)) + } + case ast.PropertyKindMethod: + e.c.emit(&defineMethodKeyed{key: key, enumerable: true}) + case ast.PropertyKindGet: + e.c.emit(&defineGetterKeyed{key: key, enumerable: true}) + case ast.PropertyKindSet: + e.c.emit(&defineSetterKeyed{key: key, enumerable: true}) + default: + e.c.assert(false, e.offset, "unknown property kind: %s", prop.Kind) + panic("unreachable") + } + } + case *ast.PropertyShort: + key := prop.Name.Name + if prop.Initializer != nil { + e.c.throwSyntaxError(int(prop.Initializer.Idx0())-1, "Invalid shorthand property initializer") + } + if e.c.scope.strict && key == "let" { + e.c.throwSyntaxError(e.offset, "'let' cannot be used as a shorthand property in strict mode") + } + e.c.compileIdentifierExpression(&prop.Name).emitGetter(true) + e.c.emit(putProp(key)) + case *ast.SpreadElement: + e.c.compileExpression(prop.Expression).emitGetter(true) + e.c.emit(copySpread) + default: + e.c.assert(false, e.offset, "unknown Property type: %T", prop) + panic("unreachable") + } + } + if !putOnStack { + e.c.emit(pop) + } +} + +func (c *compiler) compileObjectLiteral(v *ast.ObjectLiteral) compiledExpr { + r := &compiledObjectLiteral{ + expr: v, + } + r.init(c, v.Idx0()) + return r +} + +func (e *compiledArrayLiteral) emitGetter(putOnStack bool) { + e.addSrcMap() + hasSpread := false + mark := len(e.c.p.code) + e.c.emit(nil) + for _, v := range e.expr.Value { + if spread, ok := v.(*ast.SpreadElement); ok { + hasSpread = true + e.c.compileExpression(spread.Expression).emitGetter(true) + e.c.emit(pushArraySpread) + } else { + if v != nil { + e.c.emitExpr(e.c.compileExpression(v), true) + } else { + e.c.emit(loadNil) + } + e.c.emit(pushArrayItem) + } + } + var objCount uint32 + if !hasSpread { + objCount = uint32(len(e.expr.Value)) + } + e.c.p.code[mark] = newArray(objCount) + if !putOnStack { + e.c.emit(pop) + } +} + +func (c *compiler) compileArrayLiteral(v *ast.ArrayLiteral) compiledExpr { + r := &compiledArrayLiteral{ + expr: v, + } + r.init(c, v.Idx0()) + return r +} + +func (e *compiledRegexpLiteral) emitGetter(putOnStack bool) { + if putOnStack { + pattern, err := compileRegexp(e.expr.Pattern, e.expr.Flags) + if err != nil { + e.c.throwSyntaxError(e.offset, err.Error()) + } + + e.c.emit(&newRegexp{pattern: pattern, src: newStringValue(e.expr.Pattern)}) + } +} + +func (c *compiler) compileRegexpLiteral(v *ast.RegExpLiteral) compiledExpr { + r := &compiledRegexpLiteral{ + expr: v, + } + r.init(c, v.Idx0()) + return r +} + +func (c *compiler) emitCallee(callee compiledExpr) (calleeName unistring.String) { + switch callee := callee.(type) { + case *compiledDotExpr: + callee.left.emitGetter(true) + c.emit(getPropCallee(callee.name)) + case *compiledPrivateDotExpr: + callee.left.emitGetter(true) + rn, id := c.resolvePrivateName(callee.name, callee.offset) + if rn != nil { + c.emit((*getPrivatePropResCallee)(rn)) + } else { + c.emit((*getPrivatePropIdCallee)(id)) + } + case *compiledSuperDotExpr: + c.emitLoadThis() + c.emit(loadSuper) + c.emit(getPropRecvCallee(callee.name)) + case *compiledBracketExpr: + callee.left.emitGetter(true) + callee.member.emitGetter(true) + c.emit(getElemCallee) + case *compiledSuperBracketExpr: + c.emitLoadThis() + c.emit(loadSuper) + callee.member.emitGetter(true) + c.emit(getElemRecvCallee) + case *compiledIdentifierExpr: + calleeName = callee.name + callee.emitGetterAndCallee() + case *compiledOptionalChain: + c.startOptChain() + c.emitCallee(callee.expr) + c.endOptChain() + case *compiledOptional: + c.emitCallee(callee.expr) + c.block.conts = append(c.block.conts, len(c.p.code)) + c.emit(nil) + case *compiledSuperExpr: + // no-op + default: + c.emit(loadUndef) + callee.emitGetter(true) + } + return +} + +func (e *compiledCallExpr) emitGetter(putOnStack bool) { + if e.isVariadic { + e.c.emit(startVariadic) + } + calleeName := e.c.emitCallee(e.callee) + + for _, expr := range e.args { + expr.emitGetter(true) + } + + e.addSrcMap() + if _, ok := e.callee.(*compiledSuperExpr); ok { + b, eval := e.c.scope.lookupThis() + e.c.assert(eval || b != nil, e.offset, "super call, but no 'this' binding") + if eval { + e.c.emit(resolveThisDynamic{}) + } else { + b.markAccessPoint() + e.c.emit(resolveThisStack{}) + } + if e.isVariadic { + e.c.emit(superCallVariadic) + } else { + e.c.emit(superCall(len(e.args))) + } + } else if calleeName == "eval" { + foundVar := false + for sc := e.c.scope; sc != nil; sc = sc.outer { + if !foundVar && (sc.variable || sc.isFunction()) { + foundVar = true + if !sc.strict { + sc.dynamic = true + } + } + sc.dynLookup = true + } + + if e.c.scope.strict { + if e.isVariadic { + e.c.emit(callEvalVariadicStrict) + } else { + e.c.emit(callEvalStrict(len(e.args))) + } + } else { + if e.isVariadic { + e.c.emit(callEvalVariadic) + } else { + e.c.emit(callEval(len(e.args))) + } + } + } else { + if e.isVariadic { + e.c.emit(callVariadic) + } else { + e.c.emit(call(len(e.args))) + } + } + if e.isVariadic { + e.c.emit(endVariadic) + } + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *compiledCallExpr) deleteExpr() compiledExpr { + r := &defaultDeleteExpr{ + expr: e, + } + r.init(e.c, file.Idx(e.offset+1)) + return r +} + +func (c *compiler) compileSpreadCallArgument(spread *ast.SpreadElement) compiledExpr { + r := &compiledSpreadCallArgument{ + expr: c.compileExpression(spread.Expression), + } + r.init(c, spread.Idx0()) + return r +} + +func (c *compiler) compileCallee(v ast.Expression) compiledExpr { + if sup, ok := v.(*ast.SuperExpression); ok { + if s := c.scope.nearestThis(); s != nil && s.funcType == funcDerivedCtor { + e := &compiledSuperExpr{} + e.init(c, sup.Idx) + return e + } + c.throwSyntaxError(int(v.Idx0())-1, "'super' keyword unexpected here") + panic("unreachable") + } + return c.compileExpression(v) +} + +func (c *compiler) compileCallExpression(v *ast.CallExpression) compiledExpr { + + args := make([]compiledExpr, len(v.ArgumentList)) + isVariadic := false + for i, argExpr := range v.ArgumentList { + if spread, ok := argExpr.(*ast.SpreadElement); ok { + args[i] = c.compileSpreadCallArgument(spread) + isVariadic = true + } else { + args[i] = c.compileExpression(argExpr) + } + } + + r := &compiledCallExpr{ + args: args, + callee: c.compileCallee(v.Callee), + isVariadic: isVariadic, + } + r.init(c, v.LeftParenthesis) + return r +} + +func (c *compiler) compileIdentifierExpression(v *ast.Identifier) compiledExpr { + if c.scope.strict { + c.checkIdentifierName(v.Name, int(v.Idx)-1) + } + + r := &compiledIdentifierExpr{ + name: v.Name, + } + r.offset = int(v.Idx) - 1 + r.init(c, v.Idx0()) + return r +} + +func (c *compiler) compileNumberLiteral(v *ast.NumberLiteral) compiledExpr { + if c.scope.strict && len(v.Literal) > 1 && v.Literal[0] == '0' && v.Literal[1] <= '7' && v.Literal[1] >= '0' { + c.throwSyntaxError(int(v.Idx)-1, "Octal literals are not allowed in strict mode") + panic("Unreachable") + } + var val Value + switch num := v.Value.(type) { + case int64: + val = intToValue(num) + case float64: + val = floatToValue(num) + case *big.Int: + val = (*valueBigInt)(num) + default: + c.assert(false, int(v.Idx)-1, "Unsupported number literal type: %T", v.Value) + panic("unreachable") + } + r := &compiledLiteral{ + val: val, + } + r.init(c, v.Idx0()) + return r +} + +func (c *compiler) compileStringLiteral(v *ast.StringLiteral) compiledExpr { + r := &compiledLiteral{ + val: stringValueFromRaw(v.Value), + } + r.init(c, v.Idx0()) + return r +} + +func (c *compiler) compileTemplateLiteral(v *ast.TemplateLiteral) compiledExpr { + r := &compiledTemplateLiteral{} + if v.Tag != nil { + r.tag = c.compileExpression(v.Tag) + } + ce := make([]compiledExpr, len(v.Expressions)) + for i, expr := range v.Expressions { + ce[i] = c.compileExpression(expr) + } + r.expressions = ce + r.elements = v.Elements + r.init(c, v.Idx0()) + return r +} + +func (c *compiler) compileBooleanLiteral(v *ast.BooleanLiteral) compiledExpr { + var val Value + if v.Value { + val = valueTrue + } else { + val = valueFalse + } + + r := &compiledLiteral{ + val: val, + } + r.init(c, v.Idx0()) + return r +} + +func (c *compiler) compileAssignExpression(v *ast.AssignExpression) compiledExpr { + // log.Printf("compileAssignExpression(): %+v", v) + + r := &compiledAssignExpr{ + left: c.compileExpression(v.Left), + right: c.compileExpression(v.Right), + operator: v.Operator, + } + r.init(c, v.Idx0()) + return r +} + +func (e *compiledEnumGetExpr) emitGetter(putOnStack bool) { + e.c.emit(enumGet) + if !putOnStack { + e.c.emit(pop) + } +} + +func (c *compiler) compileObjectAssignmentPattern(v *ast.ObjectPattern) compiledExpr { + r := &compiledObjectAssignmentPattern{ + expr: v, + } + r.init(c, v.Idx0()) + return r +} + +func (e *compiledObjectAssignmentPattern) emitGetter(putOnStack bool) { + if putOnStack { + e.c.emit(loadUndef) + } +} + +func (c *compiler) compileArrayAssignmentPattern(v *ast.ArrayPattern) compiledExpr { + r := &compiledArrayAssignmentPattern{ + expr: v, + } + r.init(c, v.Idx0()) + return r +} + +func (e *compiledArrayAssignmentPattern) emitGetter(putOnStack bool) { + if putOnStack { + e.c.emit(loadUndef) + } +} + +func (c *compiler) emitExpr(expr compiledExpr, putOnStack bool) { + if expr.constant() { + c.emitConst(expr, putOnStack) + } else { + expr.emitGetter(putOnStack) + } +} + +type namedEmitter interface { + emitNamed(name unistring.String) +} + +func (c *compiler) emitNamed(expr compiledExpr, name unistring.String) { + if en, ok := expr.(namedEmitter); ok { + en.emitNamed(name) + } else { + expr.emitGetter(true) + } +} + +func (c *compiler) emitNamedOrConst(expr compiledExpr, name unistring.String) { + if expr.constant() { + c.emitConst(expr, true) + } else { + c.emitNamed(expr, name) + } +} + +func (e *compiledFunctionLiteral) emitNamed(name unistring.String) { + e.lhsName = name + e.emitGetter(true) +} + +func (e *compiledClassLiteral) emitNamed(name unistring.String) { + e.lhsName = name + e.emitGetter(true) +} + +func (c *compiler) emitPattern(pattern ast.Pattern, emitter func(target, init compiledExpr), putOnStack bool) { + switch pattern := pattern.(type) { + case *ast.ObjectPattern: + c.emitObjectPattern(pattern, emitter, putOnStack) + case *ast.ArrayPattern: + c.emitArrayPattern(pattern, emitter, putOnStack) + default: + c.assert(false, int(pattern.Idx0())-1, "unsupported Pattern: %T", pattern) + panic("unreachable") + } +} + +func (c *compiler) emitAssign(target ast.Expression, init compiledExpr, emitAssignSimple func(target, init compiledExpr)) { + pattern, isPattern := target.(ast.Pattern) + if isPattern { + init.emitGetter(true) + c.emitPattern(pattern, emitAssignSimple, false) + } else { + emitAssignSimple(c.compileExpression(target), init) + } +} + +func (c *compiler) emitObjectPattern(pattern *ast.ObjectPattern, emitAssign func(target, init compiledExpr), putOnStack bool) { + if pattern.Rest != nil { + c.emit(createDestructSrc) + } else { + c.emit(checkObjectCoercible) + } + for _, prop := range pattern.Properties { + switch prop := prop.(type) { + case *ast.PropertyShort: + c.emit(dup) + emitAssign(c.compileIdentifierExpression(&prop.Name), c.compilePatternInitExpr(func() { + c.emit(getProp(prop.Name.Name)) + }, prop.Initializer, prop.Idx0())) + case *ast.PropertyKeyed: + c.emit(dup) + c.compileExpression(prop.Key).emitGetter(true) + c.emit(_toPropertyKey{}) + var target ast.Expression + var initializer ast.Expression + if e, ok := prop.Value.(*ast.AssignExpression); ok { + target = e.Left + initializer = e.Right + } else { + target = prop.Value + } + c.emitAssign(target, c.compilePatternInitExpr(func() { + c.emit(getKey) + }, initializer, prop.Idx0()), emitAssign) + default: + c.throwSyntaxError(int(prop.Idx0()-1), "Unsupported AssignmentProperty type: %T", prop) + } + } + if pattern.Rest != nil { + emitAssign(c.compileExpression(pattern.Rest), c.compileEmitterExpr(func() { + c.emit(copyRest) + }, pattern.Rest.Idx0())) + c.emit(pop) + } + if !putOnStack { + c.emit(pop) + } +} + +func (c *compiler) emitArrayPattern(pattern *ast.ArrayPattern, emitAssign func(target, init compiledExpr), putOnStack bool) { + c.emit(iterate) + for _, elt := range pattern.Elements { + switch elt := elt.(type) { + case nil: + c.emit(iterGetNextOrUndef{}, pop) + case *ast.AssignExpression: + c.emitAssign(elt.Left, c.compilePatternInitExpr(func() { + c.emit(iterGetNextOrUndef{}) + }, elt.Right, elt.Idx0()), emitAssign) + default: + c.emitAssign(elt, c.compileEmitterExpr(func() { + c.emit(iterGetNextOrUndef{}) + }, elt.Idx0()), emitAssign) + } + } + if pattern.Rest != nil { + c.emitAssign(pattern.Rest, c.compileEmitterExpr(func() { + c.emit(newArrayFromIter) + }, pattern.Rest.Idx0()), emitAssign) + } else { + c.emit(enumPopClose) + } + + if !putOnStack { + c.emit(pop) + } +} + +func (e *compiledObjectAssignmentPattern) emitSetter(valueExpr compiledExpr, putOnStack bool) { + valueExpr.emitGetter(true) + e.c.emitObjectPattern(e.expr, e.c.emitPatternAssign, putOnStack) +} + +func (e *compiledArrayAssignmentPattern) emitSetter(valueExpr compiledExpr, putOnStack bool) { + valueExpr.emitGetter(true) + e.c.emitArrayPattern(e.expr, e.c.emitPatternAssign, putOnStack) +} + +type compiledPatternInitExpr struct { + baseCompiledExpr + emitSrc func() + def compiledExpr +} + +func (e *compiledPatternInitExpr) emitGetter(putOnStack bool) { + if !putOnStack { + return + } + e.emitSrc() + if e.def != nil { + mark := len(e.c.p.code) + e.c.emit(nil) + e.c.emitExpr(e.def, true) + e.c.p.code[mark] = jdef(len(e.c.p.code) - mark) + } +} + +func (e *compiledPatternInitExpr) emitNamed(name unistring.String) { + e.emitSrc() + if e.def != nil { + mark := len(e.c.p.code) + e.c.emit(nil) + e.c.emitNamedOrConst(e.def, name) + e.c.p.code[mark] = jdef(len(e.c.p.code) - mark) + } +} + +func (c *compiler) compilePatternInitExpr(emitSrc func(), def ast.Expression, idx file.Idx) compiledExpr { + r := &compiledPatternInitExpr{ + emitSrc: emitSrc, + def: c.compileExpression(def), + } + r.init(c, idx) + return r +} + +type compiledEmitterExpr struct { + baseCompiledExpr + emitter func() + namedEmitter func(name unistring.String) +} + +func (e *compiledEmitterExpr) emitGetter(putOnStack bool) { + if e.emitter != nil { + e.emitter() + } else { + e.namedEmitter("") + } + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *compiledEmitterExpr) emitNamed(name unistring.String) { + if e.namedEmitter != nil { + e.namedEmitter(name) + } else { + e.emitter() + } +} + +func (c *compiler) compileEmitterExpr(emitter func(), idx file.Idx) *compiledEmitterExpr { + r := &compiledEmitterExpr{ + emitter: emitter, + } + r.init(c, idx) + return r +} + +func (e *compiledSpreadCallArgument) emitGetter(putOnStack bool) { + e.expr.emitGetter(putOnStack) + if putOnStack { + e.c.emit(pushSpread) + } +} + +func (c *compiler) startOptChain() { + c.block = &block{ + typ: blockOptChain, + outer: c.block, + } +} + +func (c *compiler) endOptChain() { + lbl := len(c.p.code) + for _, item := range c.block.breaks { + c.p.code[item] = jopt(lbl - item) + } + for _, item := range c.block.conts { + c.p.code[item] = joptc(lbl - item) + } + c.block = c.block.outer +} + +func (e *compiledOptionalChain) emitGetter(putOnStack bool) { + e.c.startOptChain() + e.expr.emitGetter(true) + e.c.endOptChain() + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *compiledOptional) emitGetter(putOnStack bool) { + e.expr.emitGetter(putOnStack) + if putOnStack { + e.c.block.breaks = append(e.c.block.breaks, len(e.c.p.code)) + e.c.emit(nil) + } +} + +func (e *compiledAwaitExpression) emitGetter(putOnStack bool) { + e.arg.emitGetter(true) + e.c.emit(await) + if !putOnStack { + e.c.emit(pop) + } +} + +func (e *compiledYieldExpression) emitGetter(putOnStack bool) { + if e.arg != nil { + e.arg.emitGetter(true) + } else { + e.c.emit(loadUndef) + } + if putOnStack { + if e.delegate { + e.c.emit(yieldDelegateRes) + } else { + e.c.emit(yieldRes) + } + } else { + if e.delegate { + e.c.emit(yieldDelegate) + } else { + e.c.emit(yield) + } + } +} diff --git a/goja/compiler_stmt.go b/goja/compiler_stmt.go new file mode 100644 index 0000000..2d3d83b --- /dev/null +++ b/goja/compiler_stmt.go @@ -0,0 +1,1127 @@ +package goja + +import ( + "github.com/dop251/goja/ast" + "github.com/dop251/goja/file" + "github.com/dop251/goja/token" + "github.com/dop251/goja/unistring" +) + +func (c *compiler) compileStatement(v ast.Statement, needResult bool) { + + switch v := v.(type) { + case *ast.BlockStatement: + c.compileBlockStatement(v, needResult) + case *ast.ExpressionStatement: + c.compileExpressionStatement(v, needResult) + case *ast.VariableStatement: + c.compileVariableStatement(v) + case *ast.LexicalDeclaration: + c.compileLexicalDeclaration(v) + case *ast.ReturnStatement: + c.compileReturnStatement(v) + case *ast.IfStatement: + c.compileIfStatement(v, needResult) + case *ast.DoWhileStatement: + c.compileDoWhileStatement(v, needResult) + case *ast.ForStatement: + c.compileForStatement(v, needResult) + case *ast.ForInStatement: + c.compileForInStatement(v, needResult) + case *ast.ForOfStatement: + c.compileForOfStatement(v, needResult) + case *ast.WhileStatement: + c.compileWhileStatement(v, needResult) + case *ast.BranchStatement: + c.compileBranchStatement(v) + case *ast.TryStatement: + c.compileTryStatement(v, needResult) + case *ast.ThrowStatement: + c.compileThrowStatement(v) + case *ast.SwitchStatement: + c.compileSwitchStatement(v, needResult) + case *ast.LabelledStatement: + c.compileLabeledStatement(v, needResult) + case *ast.EmptyStatement: + c.compileEmptyStatement(needResult) + case *ast.FunctionDeclaration: + c.compileStandaloneFunctionDecl(v) + // note functions inside blocks are hoisted to the top of the block and are compiled using compileFunctions() + case *ast.ClassDeclaration: + c.compileClassDeclaration(v) + case *ast.WithStatement: + c.compileWithStatement(v, needResult) + case *ast.DebuggerStatement: + default: + c.assert(false, int(v.Idx0())-1, "Unknown statement type: %T", v) + panic("unreachable") + } +} + +func (c *compiler) compileLabeledStatement(v *ast.LabelledStatement, needResult bool) { + label := v.Label.Name + if c.scope.strict { + c.checkIdentifierName(label, int(v.Label.Idx)-1) + } + for b := c.block; b != nil; b = b.outer { + if b.label == label { + c.throwSyntaxError(int(v.Label.Idx-1), "Label '%s' has already been declared", label) + } + } + switch s := v.Statement.(type) { + case *ast.ForInStatement: + c.compileLabeledForInStatement(s, needResult, label) + case *ast.ForOfStatement: + c.compileLabeledForOfStatement(s, needResult, label) + case *ast.ForStatement: + c.compileLabeledForStatement(s, needResult, label) + case *ast.WhileStatement: + c.compileLabeledWhileStatement(s, needResult, label) + case *ast.DoWhileStatement: + c.compileLabeledDoWhileStatement(s, needResult, label) + default: + c.compileGenericLabeledStatement(s, needResult, label) + } +} + +func (c *compiler) updateEnterBlock(enter *enterBlock) { + scope := c.scope + stashSize, stackSize := 0, 0 + if scope.dynLookup { + stashSize = len(scope.bindings) + enter.names = scope.makeNamesMap() + } else { + for _, b := range scope.bindings { + if b.inStash { + stashSize++ + } else { + stackSize++ + } + } + } + enter.stashSize, enter.stackSize = uint32(stashSize), uint32(stackSize) +} + +func (c *compiler) compileTryStatement(v *ast.TryStatement, needResult bool) { + c.block = &block{ + typ: blockTry, + outer: c.block, + } + var lp int + var bodyNeedResult bool + var finallyBreaking *block + if v.Finally != nil { + lp, finallyBreaking = c.scanStatements(v.Finally.List) + } + if finallyBreaking != nil { + c.block.breaking = finallyBreaking + if lp == -1 { + bodyNeedResult = finallyBreaking.needResult + } + } else { + bodyNeedResult = needResult + } + lbl := len(c.p.code) + c.emit(nil) + if needResult { + c.emit(clearResult) + } + c.compileBlockStatement(v.Body, bodyNeedResult) + var catchOffset int + if v.Catch != nil { + lbl2 := len(c.p.code) // jump over the catch block + c.emit(nil) + catchOffset = len(c.p.code) - lbl + if v.Catch.Parameter != nil { + c.block = &block{ + typ: blockScope, + outer: c.block, + } + c.newBlockScope() + list := v.Catch.Body.List + funcs := c.extractFunctions(list) + if _, ok := v.Catch.Parameter.(ast.Pattern); ok { + // add anonymous binding for the catch parameter, note it must be first + c.scope.addBinding(int(v.Catch.Idx0()) - 1) + } + c.createBindings(v.Catch.Parameter, func(name unistring.String, offset int) { + if c.scope.strict { + switch name { + case "arguments", "eval": + c.throwSyntaxError(offset, "Catch variable may not be eval or arguments in strict mode") + } + } + c.scope.bindNameLexical(name, true, offset) + }) + enter := &enterBlock{} + c.emit(enter) + if pattern, ok := v.Catch.Parameter.(ast.Pattern); ok { + c.scope.bindings[0].emitGet() + c.emitPattern(pattern, func(target, init compiledExpr) { + c.emitPatternLexicalAssign(target, init) + }, false) + } + for _, decl := range funcs { + c.scope.bindNameLexical(decl.Function.Name.Name, true, int(decl.Function.Name.Idx1())-1) + } + c.compileLexicalDeclarations(list, true) + c.compileFunctions(funcs) + c.compileStatements(list, bodyNeedResult) + c.leaveScopeBlock(enter) + if c.scope.dynLookup || c.scope.bindings[0].inStash { + c.p.code[lbl+catchOffset] = &enterCatchBlock{ + names: enter.names, + stashSize: enter.stashSize, + stackSize: enter.stackSize, + } + } else { + enter.stackSize-- + } + c.popScope() + } else { + c.emit(pop) + c.compileBlockStatement(v.Catch.Body, bodyNeedResult) + } + c.p.code[lbl2] = jump(len(c.p.code) - lbl2) + } + var finallyOffset int + if v.Finally != nil { + c.emit(enterFinally{}) + finallyOffset = len(c.p.code) - lbl // finallyOffset should not include enterFinally + if bodyNeedResult && finallyBreaking != nil && lp == -1 { + c.emit(clearResult) + } + c.compileBlockStatement(v.Finally, false) + c.emit(leaveFinally{}) + } else { + c.emit(leaveTry{}) + } + c.p.code[lbl] = try{catchOffset: int32(catchOffset), finallyOffset: int32(finallyOffset)} + c.leaveBlock() +} + +func (c *compiler) addSrcMap(node ast.Node) { + c.p.addSrcMap(int(node.Idx0()) - 1) +} + +func (c *compiler) compileThrowStatement(v *ast.ThrowStatement) { + c.compileExpression(v.Argument).emitGetter(true) + c.addSrcMap(v) + c.emit(throw) +} + +func (c *compiler) compileDoWhileStatement(v *ast.DoWhileStatement, needResult bool) { + c.compileLabeledDoWhileStatement(v, needResult, "") +} + +func (c *compiler) compileLabeledDoWhileStatement(v *ast.DoWhileStatement, needResult bool, label unistring.String) { + c.block = &block{ + typ: blockLoop, + outer: c.block, + label: label, + needResult: needResult, + } + + start := len(c.p.code) + c.compileStatement(v.Body, needResult) + c.block.cont = len(c.p.code) + c.emitExpr(c.compileExpression(v.Test), true) + c.emit(jeq(start - len(c.p.code))) + c.leaveBlock() +} + +func (c *compiler) compileForStatement(v *ast.ForStatement, needResult bool) { + c.compileLabeledForStatement(v, needResult, "") +} + +func (c *compiler) compileForHeadLexDecl(decl *ast.LexicalDeclaration, needResult bool) *enterBlock { + c.block = &block{ + typ: blockIterScope, + outer: c.block, + needResult: needResult, + } + + c.newBlockScope() + enterIterBlock := &enterBlock{} + c.emit(enterIterBlock) + c.createLexicalBindings(decl) + c.compileLexicalDeclaration(decl) + return enterIterBlock +} + +func (c *compiler) compileLabeledForStatement(v *ast.ForStatement, needResult bool, label unistring.String) { + loopBlock := &block{ + typ: blockLoop, + outer: c.block, + label: label, + needResult: needResult, + } + c.block = loopBlock + + var enterIterBlock *enterBlock + switch init := v.Initializer.(type) { + case nil: + // no-op + case *ast.ForLoopInitializerLexicalDecl: + enterIterBlock = c.compileForHeadLexDecl(&init.LexicalDeclaration, needResult) + case *ast.ForLoopInitializerVarDeclList: + for _, expr := range init.List { + c.compileVarBinding(expr) + } + case *ast.ForLoopInitializerExpression: + c.compileExpression(init.Expression).emitGetter(false) + default: + c.assert(false, int(v.For)-1, "Unsupported for loop initializer: %T", init) + panic("unreachable") + } + + if needResult { + c.emit(clearResult) // initial result + } + + if enterIterBlock != nil { + c.emit(jump(1)) + } + + start := len(c.p.code) + var j int + testConst := false + if v.Test != nil { + expr := c.compileExpression(v.Test) + if expr.constant() { + r, ex := c.evalConst(expr) + if ex == nil { + if r.ToBoolean() { + testConst = true + } else { + leave := c.enterDummyMode() + c.compileStatement(v.Body, false) + if v.Update != nil { + c.compileExpression(v.Update).emitGetter(false) + } + leave() + goto end + } + } else { + expr.addSrcMap() + c.emitThrow(ex.val) + goto end + } + } else { + expr.emitGetter(true) + j = len(c.p.code) + c.emit(nil) + } + } + if needResult { + c.emit(clearResult) + } + c.compileStatement(v.Body, needResult) + loopBlock.cont = len(c.p.code) + if enterIterBlock != nil { + c.emit(jump(1)) + } + if v.Update != nil { + c.compileExpression(v.Update).emitGetter(false) + } + if enterIterBlock != nil { + if c.scope.needStash || c.scope.isDynamic() { + c.p.code[start-1] = copyStash{} + c.p.code[loopBlock.cont] = copyStash{} + } else { + if l := len(c.p.code); l > loopBlock.cont { + loopBlock.cont++ + } else { + c.p.code = c.p.code[:l-1] + } + } + } + c.emit(jump(start - len(c.p.code))) + if v.Test != nil { + if !testConst { + c.p.code[j] = jne(len(c.p.code) - j) + } + } +end: + if enterIterBlock != nil { + c.leaveScopeBlock(enterIterBlock) + c.popScope() + } + c.leaveBlock() +} + +func (c *compiler) compileForInStatement(v *ast.ForInStatement, needResult bool) { + c.compileLabeledForInStatement(v, needResult, "") +} + +func (c *compiler) compileForInto(into ast.ForInto, needResult bool) (enter *enterBlock) { + switch into := into.(type) { + case *ast.ForIntoExpression: + c.compileExpression(into.Expression).emitSetter(&c.enumGetExpr, false) + case *ast.ForIntoVar: + if c.scope.strict && into.Binding.Initializer != nil { + c.throwSyntaxError(int(into.Binding.Initializer.Idx0())-1, "for-in loop variable declaration may not have an initializer.") + } + switch target := into.Binding.Target.(type) { + case *ast.Identifier: + c.compileIdentifierExpression(target).emitSetter(&c.enumGetExpr, false) + case ast.Pattern: + c.emit(enumGet) + c.emitPattern(target, c.emitPatternVarAssign, false) + default: + c.throwSyntaxError(int(target.Idx0()-1), "unsupported for-in var target: %T", target) + } + case *ast.ForDeclaration: + + c.block = &block{ + typ: blockIterScope, + outer: c.block, + needResult: needResult, + } + + c.newBlockScope() + enter = &enterBlock{} + c.emit(enter) + switch target := into.Target.(type) { + case *ast.Identifier: + b := c.createLexicalIdBinding(target.Name, into.IsConst, int(into.Idx)-1) + c.emit(enumGet) + b.emitInitP() + case ast.Pattern: + c.createLexicalBinding(target, into.IsConst) + c.emit(enumGet) + c.emitPattern(target, func(target, init compiledExpr) { + c.emitPatternLexicalAssign(target, init) + }, false) + default: + c.assert(false, int(into.Idx)-1, "Unsupported ForBinding: %T", into.Target) + } + default: + c.assert(false, int(into.Idx0())-1, "Unsupported for-into: %T", into) + panic("unreachable") + } + + return +} + +func (c *compiler) compileLabeledForInOfStatement(into ast.ForInto, source ast.Expression, body ast.Statement, iter, needResult bool, label unistring.String) { + c.block = &block{ + typ: blockLoopEnum, + outer: c.block, + label: label, + needResult: needResult, + } + enterPos := -1 + if forDecl, ok := into.(*ast.ForDeclaration); ok { + c.block = &block{ + typ: blockScope, + outer: c.block, + needResult: false, + } + c.newBlockScope() + enterPos = len(c.p.code) + c.emit(jump(1)) + c.createLexicalBinding(forDecl.Target, forDecl.IsConst) + } + c.compileExpression(source).emitGetter(true) + if enterPos != -1 { + s := c.scope + used := len(c.block.breaks) > 0 || s.isDynamic() + if !used { + for _, b := range s.bindings { + if b.useCount() > 0 { + used = true + break + } + } + } + if used { + // We need the stack untouched because it contains the source. + // This is not the most optimal way, but it's an edge case, hopefully quite rare. + for _, b := range s.bindings { + b.moveToStash() + } + enter := &enterBlock{} + c.p.code[enterPos] = enter + c.leaveScopeBlock(enter) + } else { + c.block = c.block.outer + } + c.popScope() + } + if iter { + c.emit(iterateP) + } else { + c.emit(enumerate) + } + if needResult { + c.emit(clearResult) + } + start := len(c.p.code) + c.block.cont = start + c.emit(nil) + enterIterBlock := c.compileForInto(into, needResult) + if needResult { + c.emit(clearResult) + } + c.compileStatement(body, needResult) + if enterIterBlock != nil { + c.leaveScopeBlock(enterIterBlock) + c.popScope() + } + c.emit(jump(start - len(c.p.code))) + if iter { + c.p.code[start] = iterNext(len(c.p.code) - start) + } else { + c.p.code[start] = enumNext(len(c.p.code) - start) + } + c.emit(enumPop, jump(2)) + c.leaveBlock() + c.emit(enumPopClose) +} + +func (c *compiler) compileLabeledForInStatement(v *ast.ForInStatement, needResult bool, label unistring.String) { + c.compileLabeledForInOfStatement(v.Into, v.Source, v.Body, false, needResult, label) +} + +func (c *compiler) compileForOfStatement(v *ast.ForOfStatement, needResult bool) { + c.compileLabeledForOfStatement(v, needResult, "") +} + +func (c *compiler) compileLabeledForOfStatement(v *ast.ForOfStatement, needResult bool, label unistring.String) { + c.compileLabeledForInOfStatement(v.Into, v.Source, v.Body, true, needResult, label) +} + +func (c *compiler) compileWhileStatement(v *ast.WhileStatement, needResult bool) { + c.compileLabeledWhileStatement(v, needResult, "") +} + +func (c *compiler) compileLabeledWhileStatement(v *ast.WhileStatement, needResult bool, label unistring.String) { + c.block = &block{ + typ: blockLoop, + outer: c.block, + label: label, + needResult: needResult, + } + + if needResult { + c.emit(clearResult) + } + start := len(c.p.code) + c.block.cont = start + expr := c.compileExpression(v.Test) + testTrue := false + var j int + if expr.constant() { + if t, ex := c.evalConst(expr); ex == nil { + if t.ToBoolean() { + testTrue = true + } else { + c.compileStatementDummy(v.Body) + goto end + } + } else { + c.emitThrow(ex.val) + goto end + } + } else { + expr.emitGetter(true) + j = len(c.p.code) + c.emit(nil) + } + if needResult { + c.emit(clearResult) + } + c.compileStatement(v.Body, needResult) + c.emit(jump(start - len(c.p.code))) + if !testTrue { + c.p.code[j] = jne(len(c.p.code) - j) + } +end: + c.leaveBlock() +} + +func (c *compiler) compileEmptyStatement(needResult bool) { + if needResult { + c.emit(clearResult) + } +} + +func (c *compiler) compileBranchStatement(v *ast.BranchStatement) { + switch v.Token { + case token.BREAK: + c.compileBreak(v.Label, v.Idx) + case token.CONTINUE: + c.compileContinue(v.Label, v.Idx) + default: + c.assert(false, int(v.Idx0())-1, "Unknown branch statement token: %s", v.Token.String()) + panic("unreachable") + } +} + +func (c *compiler) findBranchBlock(st *ast.BranchStatement) *block { + switch st.Token { + case token.BREAK: + return c.findBreakBlock(st.Label, true) + case token.CONTINUE: + return c.findBreakBlock(st.Label, false) + } + return nil +} + +func (c *compiler) findBreakBlock(label *ast.Identifier, isBreak bool) (res *block) { + if label != nil { + var found *block + for b := c.block; b != nil; b = b.outer { + if res == nil { + if bb := b.breaking; bb != nil { + res = bb + if isBreak { + return + } + } + } + if b.label == label.Name { + found = b + break + } + } + if !isBreak && found != nil && found.typ != blockLoop && found.typ != blockLoopEnum { + c.throwSyntaxError(int(label.Idx)-1, "Illegal continue statement: '%s' does not denote an iteration statement", label.Name) + } + if res == nil { + res = found + } + } else { + // find the nearest loop or switch (if break) + L: + for b := c.block; b != nil; b = b.outer { + if bb := b.breaking; bb != nil { + return bb + } + switch b.typ { + case blockLoop, blockLoopEnum: + res = b + break L + case blockSwitch: + if isBreak { + res = b + break L + } + } + } + } + + return +} + +func (c *compiler) emitBlockExitCode(label *ast.Identifier, idx file.Idx, isBreak bool) *block { + block := c.findBreakBlock(label, isBreak) + if block == nil { + c.throwSyntaxError(int(idx)-1, "Could not find block") + panic("unreachable") + } + contForLoop := !isBreak && block.typ == blockLoop +L: + for b := c.block; b != block; b = b.outer { + switch b.typ { + case blockIterScope: + // blockIterScope in 'for' loops is shared across iterations, so + // continue should not pop it. + if contForLoop && b.outer == block { + break L + } + fallthrough + case blockScope: + b.breaks = append(b.breaks, len(c.p.code)) + c.emit(nil) + case blockTry: + c.emit(leaveTry{}) + case blockWith: + c.emit(leaveWith) + case blockLoopEnum: + c.emit(enumPopClose) + } + } + return block +} + +func (c *compiler) compileBreak(label *ast.Identifier, idx file.Idx) { + block := c.emitBlockExitCode(label, idx, true) + block.breaks = append(block.breaks, len(c.p.code)) + c.emit(nil) +} + +func (c *compiler) compileContinue(label *ast.Identifier, idx file.Idx) { + block := c.emitBlockExitCode(label, idx, false) + block.conts = append(block.conts, len(c.p.code)) + c.emit(nil) +} + +func (c *compiler) compileIfBody(s ast.Statement, needResult bool) { + if !c.scope.strict { + if s, ok := s.(*ast.FunctionDeclaration); ok && !s.Function.Async && !s.Function.Generator { + c.compileFunction(s) + if needResult { + c.emit(clearResult) + } + return + } + } + c.compileStatement(s, needResult) +} + +func (c *compiler) compileIfBodyDummy(s ast.Statement) { + leave := c.enterDummyMode() + defer leave() + c.compileIfBody(s, false) +} + +func (c *compiler) compileIfStatement(v *ast.IfStatement, needResult bool) { + test := c.compileExpression(v.Test) + if needResult { + c.emit(clearResult) + } + if test.constant() { + r, ex := c.evalConst(test) + if ex != nil { + test.addSrcMap() + c.emitThrow(ex.val) + return + } + if r.ToBoolean() { + c.compileIfBody(v.Consequent, needResult) + if v.Alternate != nil { + c.compileIfBodyDummy(v.Alternate) + } + } else { + c.compileIfBodyDummy(v.Consequent) + if v.Alternate != nil { + c.compileIfBody(v.Alternate, needResult) + } else { + if needResult { + c.emit(clearResult) + } + } + } + return + } + test.emitGetter(true) + jmp := len(c.p.code) + c.emit(nil) + c.compileIfBody(v.Consequent, needResult) + if v.Alternate != nil { + jmp1 := len(c.p.code) + c.emit(nil) + c.p.code[jmp] = jne(len(c.p.code) - jmp) + c.compileIfBody(v.Alternate, needResult) + c.p.code[jmp1] = jump(len(c.p.code) - jmp1) + } else { + if needResult { + c.emit(jump(2)) + c.p.code[jmp] = jne(len(c.p.code) - jmp) + c.emit(clearResult) + } else { + c.p.code[jmp] = jne(len(c.p.code) - jmp) + } + } +} + +func (c *compiler) compileReturnStatement(v *ast.ReturnStatement) { + if s := c.scope.nearestFunction(); s != nil && s.funcType == funcClsInit { + c.throwSyntaxError(int(v.Return)-1, "Illegal return statement") + } + if v.Argument != nil { + c.emitExpr(c.compileExpression(v.Argument), true) + } else { + c.emit(loadUndef) + } + for b := c.block; b != nil; b = b.outer { + switch b.typ { + case blockTry: + c.emit(saveResult, leaveTry{}, loadResult) + case blockLoopEnum: + c.emit(enumPopClose) + } + } + if s := c.scope.nearestFunction(); s != nil && s.funcType == funcDerivedCtor { + b := s.boundNames[thisBindingName] + c.assert(b != nil, int(v.Return)-1, "Derived constructor, but no 'this' binding") + b.markAccessPoint() + } + c.emit(ret) +} + +func (c *compiler) checkVarConflict(name unistring.String, offset int) { + for sc := c.scope; sc != nil; sc = sc.outer { + if b, exists := sc.boundNames[name]; exists && !b.isVar && !(b.isArg && sc != c.scope) { + c.throwSyntaxError(offset, "Identifier '%s' has already been declared", name) + } + if sc.isFunction() { + break + } + } +} + +func (c *compiler) emitVarAssign(name unistring.String, offset int, init compiledExpr) { + c.checkVarConflict(name, offset) + if init != nil { + b, noDyn := c.scope.lookupName(name) + if noDyn { + c.emitNamedOrConst(init, name) + c.p.addSrcMap(offset) + b.emitInitP() + } else { + c.emitVarRef(name, offset, b) + c.emitNamedOrConst(init, name) + c.p.addSrcMap(offset) + c.emit(initValueP) + } + } +} + +func (c *compiler) compileVarBinding(expr *ast.Binding) { + switch target := expr.Target.(type) { + case *ast.Identifier: + c.emitVarAssign(target.Name, int(target.Idx)-1, c.compileExpression(expr.Initializer)) + case ast.Pattern: + c.compileExpression(expr.Initializer).emitGetter(true) + c.emitPattern(target, c.emitPatternVarAssign, false) + default: + c.throwSyntaxError(int(target.Idx0()-1), "unsupported variable binding target: %T", target) + } +} + +func (c *compiler) emitLexicalAssign(name unistring.String, offset int, init compiledExpr) { + b := c.scope.boundNames[name] + c.assert(b != nil, offset, "Lexical declaration for an unbound name") + if init != nil { + c.emitNamedOrConst(init, name) + c.p.addSrcMap(offset) + } else { + if b.isConst { + c.throwSyntaxError(offset, "Missing initializer in const declaration") + } + c.emit(loadUndef) + } + b.emitInitP() +} + +func (c *compiler) emitPatternVarAssign(target, init compiledExpr) { + id := target.(*compiledIdentifierExpr) + c.emitVarAssign(id.name, id.offset, init) +} + +func (c *compiler) emitPatternLexicalAssign(target, init compiledExpr) { + id := target.(*compiledIdentifierExpr) + c.emitLexicalAssign(id.name, id.offset, init) +} + +func (c *compiler) emitPatternAssign(target, init compiledExpr) { + if id, ok := target.(*compiledIdentifierExpr); ok { + b, noDyn := c.scope.lookupName(id.name) + if noDyn { + c.emitNamedOrConst(init, id.name) + b.emitSetP() + } else { + c.emitVarRef(id.name, id.offset, b) + c.emitNamedOrConst(init, id.name) + c.emit(putValueP) + } + } else { + target.emitRef() + c.emitExpr(init, true) + c.emit(putValueP) + } +} + +func (c *compiler) compileLexicalBinding(expr *ast.Binding) { + switch target := expr.Target.(type) { + case *ast.Identifier: + c.emitLexicalAssign(target.Name, int(target.Idx)-1, c.compileExpression(expr.Initializer)) + case ast.Pattern: + c.compileExpression(expr.Initializer).emitGetter(true) + c.emitPattern(target, func(target, init compiledExpr) { + c.emitPatternLexicalAssign(target, init) + }, false) + default: + c.throwSyntaxError(int(target.Idx0()-1), "unsupported lexical binding target: %T", target) + } +} + +func (c *compiler) compileVariableStatement(v *ast.VariableStatement) { + for _, expr := range v.List { + c.compileVarBinding(expr) + } +} + +func (c *compiler) compileLexicalDeclaration(v *ast.LexicalDeclaration) { + for _, e := range v.List { + c.compileLexicalBinding(e) + } +} + +func (c *compiler) isEmptyResult(st ast.Statement) bool { + switch st := st.(type) { + case *ast.EmptyStatement, *ast.VariableStatement, *ast.LexicalDeclaration, *ast.FunctionDeclaration, + *ast.ClassDeclaration, *ast.BranchStatement, *ast.DebuggerStatement: + return true + case *ast.LabelledStatement: + return c.isEmptyResult(st.Statement) + case *ast.BlockStatement: + for _, s := range st.List { + if _, ok := s.(*ast.BranchStatement); ok { + return true + } + if !c.isEmptyResult(s) { + return false + } + } + return true + } + return false +} + +func (c *compiler) scanStatements(list []ast.Statement) (lastProducingIdx int, breakingBlock *block) { + lastProducingIdx = -1 + for i, st := range list { + if bs, ok := st.(*ast.BranchStatement); ok { + if blk := c.findBranchBlock(bs); blk != nil { + breakingBlock = blk + } + break + } + if !c.isEmptyResult(st) { + lastProducingIdx = i + } + } + return +} + +func (c *compiler) compileStatementsNeedResult(list []ast.Statement, lastProducingIdx int) { + if lastProducingIdx >= 0 { + for _, st := range list[:lastProducingIdx] { + if _, ok := st.(*ast.FunctionDeclaration); ok { + continue + } + c.compileStatement(st, false) + } + c.compileStatement(list[lastProducingIdx], true) + } + var leave func() + defer func() { + if leave != nil { + leave() + } + }() + for _, st := range list[lastProducingIdx+1:] { + if _, ok := st.(*ast.FunctionDeclaration); ok { + continue + } + c.compileStatement(st, false) + if leave == nil { + if _, ok := st.(*ast.BranchStatement); ok { + leave = c.enterDummyMode() + } + } + } +} + +func (c *compiler) compileStatements(list []ast.Statement, needResult bool) { + lastProducingIdx, blk := c.scanStatements(list) + if blk != nil { + needResult = blk.needResult + } + if needResult { + c.compileStatementsNeedResult(list, lastProducingIdx) + return + } + for _, st := range list { + if _, ok := st.(*ast.FunctionDeclaration); ok { + continue + } + c.compileStatement(st, false) + } +} + +func (c *compiler) compileGenericLabeledStatement(v ast.Statement, needResult bool, label unistring.String) { + c.block = &block{ + typ: blockLabel, + outer: c.block, + label: label, + needResult: needResult, + } + c.compileStatement(v, needResult) + c.leaveBlock() +} + +func (c *compiler) compileBlockStatement(v *ast.BlockStatement, needResult bool) { + var scopeDeclared bool + funcs := c.extractFunctions(v.List) + if len(funcs) > 0 { + c.newBlockScope() + scopeDeclared = true + } + c.createFunctionBindings(funcs) + scopeDeclared = c.compileLexicalDeclarations(v.List, scopeDeclared) + + var enter *enterBlock + if scopeDeclared { + c.block = &block{ + outer: c.block, + typ: blockScope, + needResult: needResult, + } + enter = &enterBlock{} + c.emit(enter) + } + c.compileFunctions(funcs) + c.compileStatements(v.List, needResult) + if scopeDeclared { + c.leaveScopeBlock(enter) + c.popScope() + } +} + +func (c *compiler) compileExpressionStatement(v *ast.ExpressionStatement, needResult bool) { + c.emitExpr(c.compileExpression(v.Expression), needResult) + if needResult { + c.emit(saveResult) + } +} + +func (c *compiler) compileWithStatement(v *ast.WithStatement, needResult bool) { + if c.scope.strict { + c.throwSyntaxError(int(v.With)-1, "Strict mode code may not include a with statement") + return + } + c.compileExpression(v.Object).emitGetter(true) + c.emit(enterWith) + c.block = &block{ + outer: c.block, + typ: blockWith, + needResult: needResult, + } + c.newBlockScope() + c.scope.dynamic = true + c.compileStatement(v.Body, needResult) + c.emit(leaveWith) + c.leaveBlock() + c.popScope() +} + +func (c *compiler) compileSwitchStatement(v *ast.SwitchStatement, needResult bool) { + c.block = &block{ + typ: blockSwitch, + outer: c.block, + needResult: needResult, + } + + c.compileExpression(v.Discriminant).emitGetter(true) + + var funcs []*ast.FunctionDeclaration + for _, s := range v.Body { + f := c.extractFunctions(s.Consequent) + funcs = append(funcs, f...) + } + var scopeDeclared bool + if len(funcs) > 0 { + c.newBlockScope() + scopeDeclared = true + c.createFunctionBindings(funcs) + } + + for _, s := range v.Body { + scopeDeclared = c.compileLexicalDeclarations(s.Consequent, scopeDeclared) + } + + var enter *enterBlock + var db *binding + if scopeDeclared { + c.block = &block{ + typ: blockScope, + outer: c.block, + needResult: needResult, + } + enter = &enterBlock{} + c.emit(enter) + // create anonymous variable for the discriminant + bindings := c.scope.bindings + var bb []*binding + if cap(bindings) == len(bindings) { + bb = make([]*binding, len(bindings)+1) + } else { + bb = bindings[:len(bindings)+1] + } + copy(bb[1:], bindings) + db = &binding{ + scope: c.scope, + isConst: true, + isStrict: true, + } + bb[0] = db + c.scope.bindings = bb + } + + c.compileFunctions(funcs) + + if needResult { + c.emit(clearResult) + } + + jumps := make([]int, len(v.Body)) + + for i, s := range v.Body { + if s.Test != nil { + if db != nil { + db.emitGet() + } else { + c.emit(dup) + } + c.compileExpression(s.Test).emitGetter(true) + c.emit(op_strict_eq) + if db != nil { + c.emit(jne(2)) + } else { + c.emit(jne(3), pop) + } + jumps[i] = len(c.p.code) + c.emit(nil) + } + } + + if db == nil { + c.emit(pop) + } + jumpNoMatch := -1 + if v.Default != -1 { + if v.Default != 0 { + jumps[v.Default] = len(c.p.code) + c.emit(nil) + } + } else { + jumpNoMatch = len(c.p.code) + c.emit(nil) + } + + for i, s := range v.Body { + if s.Test != nil || i != 0 { + c.p.code[jumps[i]] = jump(len(c.p.code) - jumps[i]) + } + c.compileStatements(s.Consequent, needResult) + } + + if jumpNoMatch != -1 { + c.p.code[jumpNoMatch] = jump(len(c.p.code) - jumpNoMatch) + } + if enter != nil { + c.leaveScopeBlock(enter) + enter.stackSize-- + c.popScope() + } + c.leaveBlock() +} + +func (c *compiler) compileClassDeclaration(v *ast.ClassDeclaration) { + c.emitLexicalAssign(v.Class.Name.Name, int(v.Class.Class)-1, c.compileClassLiteral(v.Class, false)) +} diff --git a/goja/compiler_test.go b/goja/compiler_test.go new file mode 100644 index 0000000..fd47617 --- /dev/null +++ b/goja/compiler_test.go @@ -0,0 +1,5900 @@ +package goja + +import ( + "os" + "sync" + "testing" + "unsafe" + + "github.com/dop251/goja/unistring" +) + +const TESTLIB = ` +function $ERROR(message) { + throw new Error(message); +} + +function Test262Error(message) { + this.message = message || ""; +} + +Test262Error.prototype.toString = function () { + return "Test262Error: " + this.message; +}; + +Test262Error.thrower = (message) => { + throw new Test262Error(message); +}; + +function assert(mustBeTrue, message) { + if (mustBeTrue === true) { + return; + } + + if (message === undefined) { + message = 'Expected true but got ' + String(mustBeTrue); + } + $ERROR(message); +} + +assert._isSameValue = function (a, b) { + if (a === b) { + // Handle +/-0 vs. -/+0 + return a !== 0 || 1 / a === 1 / b; + } + + // Handle NaN vs. NaN + return a !== a && b !== b; +}; + +assert.sameValue = function (actual, expected, message) { + if (assert._isSameValue(actual, expected)) { + return; + } + + if (message === undefined) { + message = ''; + } else { + message += ' '; + } + + message += 'Expected SameValue(«' + String(actual) + '», «' + String(expected) + '») to be true'; + + $ERROR(message); +}; + +assert.throws = function (expectedErrorConstructor, func, message) { + if (typeof func !== "function") { + $ERROR('assert.throws requires two arguments: the error constructor ' + + 'and a function to run'); + return; + } + if (message === undefined) { + message = ''; + } else { + message += ' '; + } + + try { + func(); + } catch (thrown) { + if (typeof thrown !== 'object' || thrown === null) { + message += 'Thrown value was not an object!'; + $ERROR(message); + } else if (thrown.constructor !== expectedErrorConstructor) { + message += 'Expected a ' + expectedErrorConstructor.name + ' but got a ' + thrown.constructor.name; + $ERROR(message); + } + return; + } + + message += 'Expected a ' + expectedErrorConstructor.name + ' to be thrown but no exception was thrown at all'; + $ERROR(message); +}; + +function compareArray(a, b) { + if (b.length !== a.length) { + return false; + } + + for (var i = 0; i < a.length; i++) { + if (b[i] !== a[i]) { + return false; + } + } + return true; +} +` + +const TESTLIBX = ` + function looksNative(fn) { + return /native code/.test(Function.prototype.toString.call(fn)); + } + + function deepEqual(a, b) { + if (typeof a === "object") { + if (typeof b === "object") { + if (a === b) { + return true; + } + if (Reflect.getPrototypeOf(a) !== Reflect.getPrototypeOf(b)) { + return false; + } + var keysA = Object.keys(a); + var keysB = Object.keys(b); + if (keysA.length !== keysB.length) { + return false; + } + if (!compareArray(keysA.sort(), keysB.sort())) { + return false; + } + for (var i = 0; i < keysA.length; i++) { + var key = keysA[i]; + if (!deepEqual(a[key], b[key])) { + return false; + } + } + return true; + } else { + return false; + } + } + return assert._isSameValue(a, b); + } + + function assertStack(e, expected) { + const lines = e.stack.split('\n'); + assert.sameValue(lines.length, expected.length + 2, "Stack lengths mismatch"); + let lnum = 1; + for (const [file, func, line, col] of expected) { + const expLine = func === "" ? + "\tat " + file + ":" + line + ":" + col + "(" : + "\tat " + func + " (" + file + ":" + line + ":" + col + "("; + assert.sameValue(lines[lnum].substring(0, expLine.length), expLine, "line " + lnum); + lnum++; + } + } +` + +var ( + // The reason it's implemented this way rather than just as _testLib = MustCompile(...) + // is because when you try to debug the compiler and set a breakpoint it gets triggered during the + // initialisation which is annoying. + _testLib, _testLibX *Program + testLibOnce, testLibXOnce sync.Once +) + +func testLib() *Program { + testLibOnce.Do(func() { + _testLib = MustCompile("testlib.js", TESTLIB, false) + }) + return _testLib +} + +func testLibX() *Program { + testLibXOnce.Do(func() { + _testLibX = MustCompile("testlibx.js", TESTLIBX, false) + }) + return _testLibX +} + +func (r *Runtime) testPrg(p *Program, expectedResult Value, t *testing.T) { + p.dumpCode(t.Logf) + v, err := r.RunProgram(p) + if err != nil { + if ex, ok := err.(*Exception); ok { + t.Fatalf("Exception: %v", ex.String()) + } + } + vm := r.vm + t.Logf("stack size: %d", len(vm.stack)) + t.Logf("stashAllocs: %d", vm.stashAllocs) + + if v == nil && expectedResult != nil || !v.SameAs(expectedResult) { + t.Fatalf("Result: %+v, expected: %+v", v, expectedResult) + } + + if vm.sp != 0 { + t.Fatalf("sp: %d", vm.sp) + } + + if l := len(vm.iterStack); l > 0 { + t.Fatalf("iter stack is not empty: %d", l) + } +} + +func (r *Runtime) testScriptWithTestLib(script string, expectedResult Value, t *testing.T) { + _, err := r.RunProgram(testLib()) + if err != nil { + t.Fatal(err) + } + + r.testScript(script, expectedResult, t) +} + +func (r *Runtime) testScriptWithTestLibX(script string, expectedResult Value, t *testing.T) { + _, err := r.RunProgram(testLib()) + if err != nil { + t.Fatal(err) + } + + _, err = r.RunProgram(testLibX()) + if err != nil { + t.Fatal(err) + } + + r.testScript(script, expectedResult, t) +} + +func (r *Runtime) testScript(script string, expectedResult Value, t *testing.T) { + r.testPrg(MustCompile("test.js", script, false), expectedResult, t) +} + +func testScript(script string, expectedResult Value, t *testing.T) { + New().testScript(script, expectedResult, t) +} + +func testScriptWithTestLib(script string, expectedResult Value, t *testing.T) { + New().testScriptWithTestLib(script, expectedResult, t) +} + +func testScriptWithTestLibX(script string, expectedResult Value, t *testing.T) { + New().testScriptWithTestLibX(script, expectedResult, t) +} + +func (r *Runtime) testAsyncFunc(src string, expectedResult Value, t *testing.T) { + v, err := r.RunScript("test.js", "(async function test() {"+src+"\n})()") + if err != nil { + t.Fatal(err) + } + promise := v.Export().(*Promise) + switch s := promise.State(); s { + case PromiseStateFulfilled: + if res := promise.Result(); res == nil && expectedResult != nil || !res.SameAs(expectedResult) { + t.Fatalf("Result: %+v, expected: %+v", res, expectedResult) + } + case PromiseStateRejected: + res := promise.Result() + if resObj, ok := res.(*Object); ok { + if stack := resObj.Get("stack"); stack != nil { + t.Fatal(stack.String()) + } + } + t.Fatal(res.String()) + default: + t.Fatalf("Unexpected promise state: %v", s) + } +} + +func (r *Runtime) testAsyncFuncWithTestLib(src string, expectedResult Value, t *testing.T) { + _, err := r.RunProgram(testLib()) + if err != nil { + t.Fatal(err) + } + + r.testAsyncFunc(src, expectedResult, t) +} + +func (r *Runtime) testAsyncFuncWithTestLibX(src string, expectedResult Value, t *testing.T) { + _, err := r.RunProgram(testLib()) + if err != nil { + t.Fatal(err) + } + + _, err = r.RunProgram(testLibX()) + if err != nil { + t.Fatal(err) + } + + r.testAsyncFunc(src, expectedResult, t) +} + +func testAsyncFunc(src string, expectedResult Value, t *testing.T) { + New().testAsyncFunc(src, expectedResult, t) +} + +func testAsyncFuncWithTestLib(src string, expectedResult Value, t *testing.T) { + New().testAsyncFuncWithTestLib(src, expectedResult, t) +} + +func testAsyncFuncWithTestLibX(src string, expectedResult Value, t *testing.T) { + New().testAsyncFuncWithTestLibX(src, expectedResult, t) +} + +func TestEmptyProgram(t *testing.T) { + const SCRIPT = ` + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestResultEmptyBlock(t *testing.T) { + const SCRIPT = ` + undefined; + {} + ` + testScript(SCRIPT, _undefined, t) +} + +func TestResultVarDecl(t *testing.T) { + const SCRIPT = ` + 7; var x = 1; + ` + testScript(SCRIPT, valueInt(7), t) +} + +func TestResultLexDecl(t *testing.T) { + const SCRIPT = ` + 7; {let x = 1}; + ` + testScript(SCRIPT, valueInt(7), t) +} + +func TestResultLexDeclBreak(t *testing.T) { + const SCRIPT = ` + L:{ 7; {let x = 1; break L;}}; + ` + testScript(SCRIPT, valueInt(7), t) +} + +func TestResultLexDeclNested(t *testing.T) { + const SCRIPT = ` + 7; {let x = (function() { return eval("8; {let y = 9}")})()}; + ` + testScript(SCRIPT, valueInt(7), t) +} + +func TestErrorProto(t *testing.T) { + const SCRIPT = ` + var e = new TypeError(); + e.name; + ` + + testScript(SCRIPT, asciiString("TypeError"), t) +} + +func TestThis1(t *testing.T) { + const SCRIPT = ` + function independent() { + return this.prop; + } + var o = {}; + o.b = {g: independent, prop: 42}; + + o.b.g(); + ` + testScript(SCRIPT, intToValue(42), t) +} + +func TestThis2(t *testing.T) { + const SCRIPT = ` +var o = { + prop: 37, + f: function() { + return this.prop; + } +}; + +o.f(); +` + + testScript(SCRIPT, intToValue(37), t) +} + +func TestThisStrict(t *testing.T) { + const SCRIPT = ` + "use strict"; + + Object.defineProperty(Object.prototype, "x", { get: function () { return this; } }); + + (5).x === 5; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestThisNoStrict(t *testing.T) { + const SCRIPT = ` + Object.defineProperty(Object.prototype, "x", { get: function () { return this; } }); + + (5).x == 5; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestNestedFuncVarResolution(t *testing.T) { + const SCRIPT = ` + (function outer() { + var v = 42; + function inner() { + return v; + } + return inner(); + })(); +` + testScript(SCRIPT, valueInt(42), t) +} + +func TestNestedFuncVarResolution1(t *testing.T) { + const SCRIPT = ` + function outer(argOuter) { + var called = 0; + var inner = function(argInner) { + if (arguments.length !== 1) { + throw new Error(); + } + called++; + if (argOuter !== 1) { + throw new Error("argOuter"); + } + if (argInner !== 2) { + throw new Error("argInner"); + } + }; + inner(2); + } + outer(1); + ` + testScript(SCRIPT, _undefined, t) +} + +func TestCallFewerArgs(t *testing.T) { + const SCRIPT = ` +function A(a, b, c) { + return String(a) + " " + String(b) + " " + String(c); +} + +A(1, 2); +` + testScript(SCRIPT, asciiString("1 2 undefined"), t) +} + +func TestCallFewerArgsClosureNoArgs(t *testing.T) { + const SCRIPT = ` + var x; + function A(a, b, c) { + var y = a; + x = function() { return " " + y }; + return String(a) + " " + String(b) + " " + String(c); + } + + A(1, 2) + x(); +` + testScript(SCRIPT, asciiString("1 2 undefined 1"), t) +} + +func TestCallFewerArgsClosureArgs(t *testing.T) { + const SCRIPT = ` + var x; + function A(a, b, c) { + var y = b; + x = function() { return " " + a + " " + y }; + return String(a) + " " + String(b) + " " + String(c); + } + + A(1, 2) + x(); +` + testScript(SCRIPT, asciiString("1 2 undefined 1 2"), t) +} + +func TestCallMoreArgs(t *testing.T) { + const SCRIPT = ` +function A(a, b) { + var c = 4; + return a - b + c; +} + +A(1, 2, 3); +` + testScript(SCRIPT, intToValue(3), t) +} + +func TestCallMoreArgsDynamic(t *testing.T) { + const SCRIPT = ` +function A(a, b) { + var c = 4; + if (false) { + eval(""); + } + return a - b + c; +} + +A(1, 2, 3); +` + testScript(SCRIPT, intToValue(3), t) +} + +func TestCallLessArgsDynamic(t *testing.T) { + const SCRIPT = ` +function A(a, b, c) { + // Make it stashful + function B() { + return a; + } + return String(a) + " " + String(b) + " " + String(c); +} + +A(1, 2); +` + testScript(SCRIPT, asciiString("1 2 undefined"), t) +} + +func TestCallLessArgsDynamicLocalVar(t *testing.T) { + const SCRIPT = ` + function f(param) { + var a = 42; + if (false) { + eval(""); + } + return a; + } + f(); +` + + testScript(SCRIPT, intToValue(42), t) +} + +/* +func TestFib(t *testing.T) { + testScript(TEST_FIB, valueInt(9227465), t) +} +*/ + +func TestNativeCall(t *testing.T) { + const SCRIPT = ` + var o = Object(1); + Object.defineProperty(o, "test", {value: 42}); + o.test; + ` + testScript(SCRIPT, intToValue(42), t) +} + +func TestJSCall(t *testing.T) { + const SCRIPT = ` + function getter() { + return this.x; + } + var o = Object(1); + o.x = 42; + Object.defineProperty(o, "test", {get: getter}); + o.test; + ` + testScript(SCRIPT, intToValue(42), t) + +} + +func TestLoop1(t *testing.T) { + const SCRIPT = ` + function A() { + var x = 1; + for (var i = 0; i < 1; i++) { + var x = 2; + } + return x; + } + + A(); + ` + testScript(SCRIPT, intToValue(2), t) +} + +func TestLoopBreak(t *testing.T) { + const SCRIPT = ` + function A() { + var x = 1; + for (var i = 0; i < 1; i++) { + break; + var x = 2; + } + return x; + } + + A(); + ` + testScript(SCRIPT, intToValue(1), t) +} + +func TestForLoopOptionalExpr(t *testing.T) { + const SCRIPT = ` + function A() { + var x = 1; + for (;;) { + break; + var x = 2; + } + return x; + } + + A(); + ` + testScript(SCRIPT, intToValue(1), t) +} + +func TestBlockBreak(t *testing.T) { + const SCRIPT = ` + var rv = 0; + B1: { + rv = 1; + B2: { + rv = 2; + break B1; + } + rv = 3; + } + rv; + ` + testScript(SCRIPT, intToValue(2), t) + +} + +func TestTry(t *testing.T) { + const SCRIPT = ` + function A() { + var x = 1; + try { + x = 2; + } catch(e) { + x = 3; + } finally { + x = 4; + } + return x; + } + + A(); + ` + testScript(SCRIPT, intToValue(4), t) +} + +func TestTryOptionalCatchBinding(t *testing.T) { + const SCRIPT = ` + try { + throw null; + } catch { + } + ` + testScript(SCRIPT, _undefined, t) +} + +func TestTryCatch(t *testing.T) { + const SCRIPT = ` + function A() { + var x; + try { + throw 4; + } catch(e) { + x = e; + } + return x; + } + + A(); + ` + testScript(SCRIPT, intToValue(4), t) +} + +func TestTryCatchDirectEval(t *testing.T) { + const SCRIPT = ` + function A() { + var x; + try { + throw 4; + } catch(e) { + eval("x = e"); + } + return x; + } + + A(); + ` + testScript(SCRIPT, intToValue(4), t) +} + +func TestTryExceptionInCatch(t *testing.T) { + const SCRIPT = ` + function A() { + var x; + try { + throw 4; + } catch(e) { + throw 5; + } + return x; + } + + var rv; + try { + A(); + } catch (e) { + rv = e; + } + rv; + ` + testScript(SCRIPT, intToValue(5), t) +} + +func TestTryContinueInCatch(t *testing.T) { + const SCRIPT = ` + var c3 = 0, fin3 = 0; + while (c3 < 2) { + try { + throw "ex1"; + } catch(er1) { + c3 += 1; + continue; + } finally { + fin3 = 1; + } + fin3 = 0; + } + + fin3; + ` + testScript(SCRIPT, intToValue(1), t) +} + +func TestContinueInWith(t *testing.T) { + const SCRIPT = ` + var x; + var o = {x: 0}; + for (var i = 0; i < 2; i++) { + with(o) { + x = i; + if (i === 0) { + continue; + } + } + break; + } + x; + ` + testScript(SCRIPT, _undefined, t) +} + +func TestTryContinueInFinally(t *testing.T) { + const SCRIPT = ` + var c3 = 0, fin3 = 0; + while (c3 < 2) { + try { + throw "ex1"; + } catch(er1) { + c3 += 1; + } finally { + fin3 = 1; + continue; + } + fin3 = 0; + } + + fin3; + ` + testScript(SCRIPT, intToValue(1), t) +} + +func TestTryBreakFinallyContinue(t *testing.T) { + const SCRIPT = ` + for (var i = 0; i < 3; i++) { + try { + break; + } finally { + continue; + } + } + ` + testScript(SCRIPT, _undefined, t) +} + +func TestTryBreakFinallyContinueWithResult(t *testing.T) { + const SCRIPT = ` + for (var i = 0; i < 3; i++) { + try { + true; + break; + } finally { + continue; + } + } + ` + testScript(SCRIPT, _undefined, t) +} + +func TestTryBreakFinallyContinueWithResult1(t *testing.T) { + const SCRIPT = ` + for (var i = 0; i < 3; i++) { + try { + true; + break; + } finally { + var x = 1; + continue; + } + } + ` + testScript(SCRIPT, _undefined, t) +} + +func TestTryBreakFinallyContinueWithResultNested(t *testing.T) { + const SCRIPT = ` +LOOP: + for (var i = 0; i < 3; i++) { + try { + if (true) { + false; break; + } + } finally { + if (true) { + true; continue; + } + } + } + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestTryBreakOuterFinallyContinue(t *testing.T) { + const SCRIPT = ` + let iCount = 0, jCount = 0; + OUTER: for (let i = 0; i < 1; i++) { + iCount++; + for (let j = 0; j < 2; j++) { + jCount++; + try { + break OUTER; + } finally { + continue; + } + } + } + ""+iCount+jCount; + ` + testScript(SCRIPT, asciiString("12"), t) +} + +func TestTryIllegalContinueWithFinallyOverride(t *testing.T) { + const SCRIPT = ` + L: { + while (Math.random() > 0.5) { + try { + continue L; + } finally { + break; + } + } + } + ` + _, err := Compile("", SCRIPT, false) + if err == nil { + t.Fatal("expected error") + } +} + +func TestTryIllegalContinueWithFinallyOverrideNoLabel(t *testing.T) { + const SCRIPT = ` + L: { + try { + continue; + } finally { + break L; + } + } + ` + _, err := Compile("", SCRIPT, false) + if err == nil { + t.Fatal("expected error") + } +} + +func TestTryIllegalContinueWithFinallyOverrideDummy(t *testing.T) { + const SCRIPT = ` + L: { + while (false) { + try { + continue L; + } finally { + break; + } + } + } + ` + _, err := Compile("", SCRIPT, false) + if err == nil { + t.Fatal("expected error") + } +} + +func TestTryNoResult(t *testing.T) { + const SCRIPT = ` + true; + L: + try { + break L; + } finally { + } + ` + testScript(SCRIPT, _undefined, t) +} + +func TestCatchLexicalEnv(t *testing.T) { + const SCRIPT = ` + function F() { + try { + throw 1; + } catch (e) { + var x = e; + } + return x; + } + + F(); + ` + testScript(SCRIPT, intToValue(1), t) +} + +func TestThrowType(t *testing.T) { + const SCRIPT = ` + function Exception(message) { + this.message = message; + } + + + function A() { + try { + throw new Exception("boo!"); + } catch(e) { + return e; + } + } + var thrown = A(); + thrown !== null && typeof thrown === "object" && thrown.constructor === Exception; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestThrowConstructorName(t *testing.T) { + const SCRIPT = ` + function Exception(message) { + this.message = message; + } + + + function A() { + try { + throw new Exception("boo!"); + } catch(e) { + return e; + } + } + A().constructor.name; + ` + + testScript(SCRIPT, asciiString("Exception"), t) +} + +func TestThrowNativeConstructorName(t *testing.T) { + const SCRIPT = ` + + + function A() { + try { + throw new TypeError(); + } catch(e) { + return e; + } + } + A().constructor.name; + ` + + testScript(SCRIPT, asciiString("TypeError"), t) +} + +func TestEmptyTryNoCatch(t *testing.T) { + const SCRIPT = ` + var called = false; + try { + } finally { + called = true; + } + called; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestTryReturnFromCatch(t *testing.T) { + const SCRIPT = ` + function f(o) { + var x = 42; + + function innerf(o) { + try { + throw o; + } catch (e) { + return x; + } + } + + return innerf(o); + } + f({}); + ` + + testScript(SCRIPT, valueInt(42), t) +} + +func TestTryCompletionResult(t *testing.T) { + const SCRIPT = ` + 99; do { -99; try { 39 } catch (e) { -1 } finally { break; -2 }; } while (false); + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestIfElse(t *testing.T) { + const SCRIPT = ` + var rv; + if (rv === undefined) { + rv = "passed"; + } else { + rv = "failed"; + } + rv; + ` + + testScript(SCRIPT, asciiString("passed"), t) +} + +func TestIfElseRetVal(t *testing.T) { + const SCRIPT = ` + var x; + if (x === undefined) { + "passed"; + } else { + "failed"; + } + ` + + testScript(SCRIPT, asciiString("passed"), t) +} + +func TestWhileReturnValue(t *testing.T) { + const SCRIPT = ` + var x = 0; + while(true) { + x = 1; + break; + } + ` + testScript(SCRIPT, intToValue(1), t) +} + +func TestIfElseLabel(t *testing.T) { + const SCRIPT = ` + var x = 0; + abc: if (true) { + x = 1; + break abc; + } + ` + testScript(SCRIPT, intToValue(1), t) +} + +func TestIfMultipleLabels(t *testing.T) { + const SCRIPT = ` + var x = 0; + xyz:abc: if (true) { + break xyz; + } + ` + testScript(SCRIPT, _undefined, t) +} + +func TestBreakOutOfTry(t *testing.T) { + const SCRIPT = ` + function A() { + var x = 1; + B: { + try { + x = 2; + } catch(e) { + x = 3; + } finally { + break B; + x = 4; + } + } + return x; + } + + A(); + ` + testScript(SCRIPT, intToValue(2), t) +} + +func TestReturnOutOfTryNested(t *testing.T) { + const SCRIPT = ` + function A() { + function nested() { + try { + return 1; + } catch(e) { + return 2; + } + } + return nested(); + } + + A(); + ` + testScript(SCRIPT, intToValue(1), t) +} + +func TestReturnOutOfTryWithFinally(t *testing.T) { + const SCRIPT = ` + function test() { + try { + return 'Hello, world!'; + } finally { + const dummy = 'unexpected'; + } + } + test(); + ` + testScript(SCRIPT, asciiString("Hello, world!"), t) +} + +func TestContinueLoop(t *testing.T) { + const SCRIPT = ` + function A() { + var r = 0; + for (var i = 0; i < 5; i++) { + if (i > 1) { + continue; + } + r++; + } + return r; + } + + A(); + ` + testScript(SCRIPT, intToValue(2), t) +} + +func TestContinueOutOfTry(t *testing.T) { + const SCRIPT = ` + function A() { + var r = 0; + for (var i = 0; i < 5; i++) { + try { + if (i > 1) { + continue; + } + } catch(e) { + return 99; + } + r++; + } + return r; + } + + A(); + ` + testScript(SCRIPT, intToValue(2), t) +} + +func TestThisInCatch(t *testing.T) { + const SCRIPT = ` + function O() { + try { + f(); + } catch (e) { + this.value = e.toString(); + } + } + + function f() { + throw "ex"; + } + + var o = new O(); + o.value; + ` + testScript(SCRIPT, asciiString("ex"), t) +} + +func TestNestedTry(t *testing.T) { + const SCRIPT = ` + var ex; + try { + throw "ex1"; + } catch (er1) { + try { + throw "ex2"; + } catch (er1) { + ex = er1; + } + } + ex; + ` + testScript(SCRIPT, asciiString("ex2"), t) +} + +func TestNestedTryInStashlessFunc(t *testing.T) { + const SCRIPT = ` + function f() { + var ex1, ex2; + try { + throw "ex1"; + } catch (er1) { + try { + throw "ex2"; + } catch (er1) { + ex2 = er1; + } + ex1 = er1; + } + return ex1 == "ex1" && ex2 == "ex2"; + } + f(); + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestEvalLexicalDecl(t *testing.T) { + const SCRIPT = ` + eval("let x = true; x;"); + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestEvalInCatchInStashlessFunc(t *testing.T) { + const SCRIPT = ` + function f() { + var ex; + try { + throw "ex1"; + } catch (er1) { + eval("ex = er1"); + } + return ex; + } + f(); + ` + testScript(SCRIPT, asciiString("ex1"), t) +} + +func TestCatchClosureInStashlessFunc(t *testing.T) { + const SCRIPT = ` + function f() { + var ex; + try { + throw "ex1"; + } catch (er1) { + return function() { + return er1; + } + } + } + f()(); + ` + testScript(SCRIPT, asciiString("ex1"), t) +} + +func TestCatchVarNotUsedInStashlessFunc(t *testing.T) { + const SCRIPT = ` + function f() { + var ex; + try { + throw "ex1"; + } catch (er1) { + ex = "ok"; + } + return ex; + } + f(); + ` + testScript(SCRIPT, asciiString("ok"), t) +} + +func TestNew(t *testing.T) { + const SCRIPT = ` + function O() { + this.x = 42; + } + + new O().x; + ` + + testScript(SCRIPT, intToValue(42), t) +} + +func TestStringConstructor(t *testing.T) { + const SCRIPT = ` + function F() { + return String(33) + " " + String("cows"); + } + + F(); + ` + testScript(SCRIPT, asciiString("33 cows"), t) +} + +func TestError(t *testing.T) { + const SCRIPT = ` + function F() { + return new Error("test"); + } + + var e = F(); + e.message == "test" && e.name == "Error"; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestTypeError(t *testing.T) { + const SCRIPT = ` + function F() { + return new TypeError("test"); + } + + var e = F(); + e.message == "test" && e.name == "TypeError"; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestToString(t *testing.T) { + const SCRIPT = ` + var o = {x: 42}; + o.toString = function() { + return String(this.x); + } + + var o1 = {}; + o.toString() + " ### " + o1.toString(); + ` + testScript(SCRIPT, asciiString("42 ### [object Object]"), t) +} + +func TestEvalOrder(t *testing.T) { + const SCRIPT = ` + var o = {f: function() {return 42}, x: 0}; + var trace = ""; + + function F1() { + trace += "First!"; + return o; + } + + function F2() { + trace += "Second!"; + return "f"; + } + + function F3() { + trace += "Third!"; + } + + var rv = F1()[F2()](F3()); + rv += trace; + rv; + ` + + testScript(SCRIPT, asciiString("42First!Second!Third!"), t) +} + +func TestPostfixIncBracket(t *testing.T) { + const SCRIPT = ` + var o = {x: 42}; + var trace = ""; + + function F1() { + trace += "First!"; + return o; + } + + function F2() { + trace += "Second!"; + return "x"; + } + + + var rv = F1()[F2()]++; + rv + trace + o.x; + ` + testScript(SCRIPT, asciiString("42First!Second!43"), t) +} + +func TestPostfixIncDot(t *testing.T) { + const SCRIPT = ` + var o = {x: 42}; + var trace = ""; + + function F1() { + trace += "First!"; + return o; + } + + var rv = F1().x++; + rv + trace + o.x; + ` + testScript(SCRIPT, asciiString("42First!43"), t) +} + +func TestPrefixIncBracket(t *testing.T) { + const SCRIPT = ` + var o = {x: 42}; + var trace = ""; + + function F1() { + trace += "First!"; + return o; + } + + function F2() { + trace += "Second!"; + return "x"; + } + + + var rv = ++F1()[F2()]; + rv + trace + o.x; + ` + testScript(SCRIPT, asciiString("43First!Second!43"), t) +} + +func TestPrefixIncDot(t *testing.T) { + const SCRIPT = ` + var o = {x: 42}; + var trace = ""; + + function F1() { + trace += "First!"; + return o; + } + + var rv = ++F1().x; + rv + trace + o.x; + ` + testScript(SCRIPT, asciiString("43First!43"), t) +} + +func TestPostDecObj(t *testing.T) { + const SCRIPT = ` + var object = {valueOf: function() {return 1}}; + var y = object--; + var ok = false; + if (y === 1) { + ok = true; + } + ok; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestPropAcc1(t *testing.T) { + const SCRIPT = ` + 1..toString() + ` + + testScript(SCRIPT, asciiString("1"), t) +} + +func TestEvalDirect(t *testing.T) { + const SCRIPT = ` + var rv = false; + function foo(){ rv = true; } + + var o = { }; + function f() { + try { + eval("o.bar( foo() );"); + } catch (e) { + } + } + f(); + rv; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestEvalRet(t *testing.T) { + const SCRIPT = ` + eval("for (var i = 0; i < 3; i++) {i}") + ` + + testScript(SCRIPT, valueInt(2), t) +} + +func TestEvalFunctionDecl(t *testing.T) { + const SCRIPT = ` + eval("function F() {}") + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestEvalFunctionExpr(t *testing.T) { + const SCRIPT = ` + eval("(function F() {return 42;})")() + ` + + testScript(SCRIPT, intToValue(42), t) +} + +func TestEvalDirectScope(t *testing.T) { + const SCRIPT = ` + var __10_4_2_1_3 = "str"; + function testcase() { + var __10_4_2_1_3 = "str1"; + try { + throw "error"; + } catch (e) { + var __10_4_2_1_3 = "str2"; + return eval("__10_4_2_1_3"); + } + } + testcase(); + ` + + testScript(SCRIPT, asciiString("str2"), t) +} + +func TestEvalDirectScope1(t *testing.T) { + const SCRIPT = ` + 'use strict'; + var __10_4_2_1_5 = "str"; + function testcase() { + var __10_4_2_1_5 = "str1"; + var r = eval("\ + var __10_4_2_1_5 = \'str2\'; \ + eval(\"\'str2\' === __10_4_2_1_5\")\ + "); + return r; + } + testcase(); + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestEvalDirectCreateBinding(t *testing.T) { + const SCRIPT = ` + function f() { + eval("var x = true"); + return x; + } + var res = f(); + var thrown = false; + try { + x; + } catch(e) { + if (e instanceof ReferenceError) { + thrown = true; + } else { + throw e; + } + } + res && thrown; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestEvalDirectCreateBinding1(t *testing.T) { + const SCRIPT = ` + function f() { + eval("let x = 1; var y = 2; function f1() {return x};"); + assert.throws(ReferenceError, function() { x }); + return ""+y+f1(); + } + f(); + ` + + testScriptWithTestLib(SCRIPT, asciiString("21"), t) +} + +func TestEvalDirectCreateBinding3(t *testing.T) { + const SCRIPT = ` + function f() { + let x; + try { + eval("var y=1, x=2"); + } catch(e) {} + return y; + } + assert.throws(ReferenceError, f); + ` + + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestEvalGlobalStrict(t *testing.T) { + const SCRIPT = ` + 'use strict'; + var evalStr = + 'for (var x in this) {\n'+ + ' if ( x === \'Math\' ) {\n'+ + ' }\n'+ + '}\n'; + + eval(evalStr); + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestEvalEmptyStrict(t *testing.T) { + const SCRIPT = ` + 'use strict'; + eval(""); + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestEvalFuncDecl(t *testing.T) { + const SCRIPT = ` + 'use strict'; + var funcA = eval("function __funcA(__arg){return __arg;}; __funcA"); + typeof funcA; + ` + + testScript(SCRIPT, asciiString("function"), t) +} + +func TestGetAfterSet(t *testing.T) { + const SCRIPT = ` + function f() { + var x = 1; + return x; + } + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestForLoopRet(t *testing.T) { + const SCRIPT = ` + for (var i = 0; i < 20; i++) { if (i > 2) {break;} else { i }} + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestForLoopRet1(t *testing.T) { + const SCRIPT = ` + for (var i = 0; i < 20; i++) { if (i > 2) {42;; {L:{break;}}} else { i }} + ` + + testScript(SCRIPT, intToValue(42), t) +} + +func TestForInLoopRet(t *testing.T) { + const SCRIPT = ` + var o = [1, 2, 3, 4]; + for (var i in o) { if (i > 2) {break;} else { i }} + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestForInLoopRet1(t *testing.T) { + const SCRIPT = ` + var o = {}; + o.x = 1; + o.y = 2; + for (var i in o) { + true; + } + + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestDoWhileLoopRet(t *testing.T) { + const SCRIPT = ` + var i = 0; + do { + if (i > 2) { + break; + } else { + i; + } + } while (i++ < 20); + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestDoWhileContinueRet(t *testing.T) { + const SCRIPT = ` + var i = 0; + do { + if (i > 2) { + true; + continue; + } else { + i; + } + } while (i++ < 20); + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestWhileLoopRet(t *testing.T) { + const SCRIPT = ` + var i; while (i < 20) { if (i > 2) {break;} else { i++ }} + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestLoopRet1(t *testing.T) { + const SCRIPT = ` + for (var i = 0; i < 20; i++) { } + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestInstanceof(t *testing.T) { + const SCRIPT = ` + var rv; + try { + true(); + } catch (e) { + rv = e instanceof TypeError; + } + rv; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestStrictAssign(t *testing.T) { + const SCRIPT = ` + 'use strict'; + var rv; + var called = false; + function F() { + called = true; + return 1; + } + try { + x = F(); + } catch (e) { + rv = e instanceof ReferenceError; + } + rv + " " + called; + ` + + testScript(SCRIPT, asciiString("true true"), t) +} + +func TestStrictScope(t *testing.T) { + const SCRIPT = ` + var rv; + var called = false; + function F() { + 'use strict'; + x = 1; + } + try { + F(); + } catch (e) { + rv = e instanceof ReferenceError; + } + x = 1; + rv + " " + x; + ` + + testScript(SCRIPT, asciiString("true 1"), t) +} + +func TestStringObj(t *testing.T) { + const SCRIPT = ` + var s = new String("test"); + s[0] + s[2] + s[1]; + ` + + testScript(SCRIPT, asciiString("tse"), t) +} + +func TestStringPrimitive(t *testing.T) { + const SCRIPT = ` + var s = "test"; + s[0] + s[2] + s[1]; + ` + + testScript(SCRIPT, asciiString("tse"), t) +} + +func TestCallGlobalObject(t *testing.T) { + const SCRIPT = ` + var rv; + try { + this(); + } catch (e) { + rv = e instanceof TypeError + } + rv; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestFuncLength(t *testing.T) { + const SCRIPT = ` + function F(x, y) { + + } + F.length + ` + + testScript(SCRIPT, intToValue(2), t) +} + +func TestNativeFuncLength(t *testing.T) { + const SCRIPT = ` + eval.length + Object.defineProperty.length + String.length + ` + + testScript(SCRIPT, intToValue(5), t) +} + +func TestArguments(t *testing.T) { + const SCRIPT = ` + function F() { + return arguments.length + " " + arguments[1]; + } + + F(1,2,3) + ` + + testScript(SCRIPT, asciiString("3 2"), t) +} + +func TestArgumentsPut(t *testing.T) { + const SCRIPT = ` + function F(x, y) { + arguments[0] -= arguments[1]; + return x; + } + + F(5, 2) + ` + + testScript(SCRIPT, intToValue(3), t) +} + +func TestArgumentsPutStrict(t *testing.T) { + const SCRIPT = ` + function F(x, y) { + 'use strict'; + arguments[0] -= arguments[1]; + return x; + } + + F(5, 2) + ` + + testScript(SCRIPT, intToValue(5), t) +} + +func TestArgumentsExtra(t *testing.T) { + const SCRIPT = ` + function F(x, y) { + return arguments[2]; + } + + F(1, 2, 42) + ` + + testScript(SCRIPT, intToValue(42), t) +} + +func TestArgumentsExist(t *testing.T) { + const SCRIPT = ` + function F(x, arguments) { + return arguments; + } + + F(1, 42) + ` + + testScript(SCRIPT, intToValue(42), t) +} + +func TestArgumentsDelete(t *testing.T) { + const SCRIPT = ` + function f(x) { + delete arguments[0]; + arguments[0] = 42; + return x; + } + f(1) + ` + + testScript(SCRIPT, intToValue(1), t) +} + +func TestArgumentsInEval(t *testing.T) { + const SCRIPT = ` + function f() { + return eval("arguments"); + } + f(1)[0]; + ` + + testScript(SCRIPT, intToValue(1), t) +} + +func TestArgumentsRedeclareInEval(t *testing.T) { + const SCRIPT = ` + assert.sameValue("arguments" in this, false, "No global 'arguments' binding"); + + function f(p = eval("var arguments = 'param'"), arguments) {} + assert.throws(SyntaxError, f); + + assert.sameValue("arguments" in this, false, "No global 'arguments' binding"); + ` + + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestArgumentsRedeclareArrow(t *testing.T) { + const SCRIPT = ` + const oldArguments = globalThis.arguments; + let count = 0; + const f = (p = eval("var arguments = 'param'"), q = () => arguments) => { + var arguments = "local"; + assert.sameValue(arguments, "local", "arguments"); + assert.sameValue(q(), "param", "q"); + count++; + } + f(); + assert.sameValue(count, 1); + assert.sameValue(globalThis.arguments, oldArguments, "globalThis.arguments unchanged"); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestEvalParamWithDef(t *testing.T) { + const SCRIPT = ` + function f(param = 0) { + eval("var param = 1"); + return param; + } + f(); + ` + + testScript(SCRIPT, valueInt(1), t) +} + +func TestArgumentsRedefinedAsLetDyn(t *testing.T) { + const SCRIPT = ` + function f() { + let arguments; + eval(""); // force dynamic scope + return arguments; + } + + f(1,2); + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestWith(t *testing.T) { + const SCRIPT = ` + var b = 1; + var o = {a: 41}; + with(o) { + a += b; + } + o.a; + + ` + + testScript(SCRIPT, intToValue(42), t) +} + +func TestWithInFunc(t *testing.T) { + const SCRIPT = ` + function F() { + var b = 1; + var c = 0; + var o = {a: 40, c: 1}; + with(o) { + a += b + c; + } + return o.a; + } + + F(); + ` + + testScript(SCRIPT, intToValue(42), t) +} + +func TestAssignNonExtendable(t *testing.T) { + const SCRIPT = ` + 'use strict'; + + function F() { + this.x = 1; + } + + var o = new F(); + Object.preventExtensions(o); + o.x = 42; + o.x; + ` + + testScript(SCRIPT, intToValue(42), t) +} + +func TestAssignNonExtendable1(t *testing.T) { + const SCRIPT = ` + 'use strict'; + + function F() { + } + + var o = new F(); + var rv; + + Object.preventExtensions(o); + try { + o.x = 42; + } catch (e) { + rv = e.constructor === TypeError; + } + + rv += " " + o.x; + rv; + ` + + testScript(SCRIPT, asciiString("true undefined"), t) +} + +func TestAssignStrict(t *testing.T) { + const SCRIPT = ` + 'use strict'; + + try { + eval("eval = 42"); + } catch(e) { + var rv = e instanceof SyntaxError + } + rv; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestIllegalArgmentName(t *testing.T) { + const SCRIPT = ` + 'use strict'; + + try { + eval("function F(eval) {}"); + } catch (e) { + var rv = e instanceof SyntaxError + } + rv; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestFunction(t *testing.T) { + const SCRIPT = ` + + var f0 = Function(""); + var f1 = Function("return ' one'"); + var f2 = Function("arg", "return ' ' + arg"); + f0() + f1() + f2("two"); + ` + + testScript(SCRIPT, asciiString("undefined one two"), t) +} + +func TestFunction1(t *testing.T) { + const SCRIPT = ` + + var f = function f1(count) { + if (count == 0) { + return true; + } + return f1(count-1); + } + + f(1); + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestFunction2(t *testing.T) { + const SCRIPT = ` + var trace = ""; + function f(count) { + trace += "f("+count+")"; + if (count == 0) { + return; + } + return f(count-1); + } + + function f1() { + trace += "f1"; + } + + var f2 = f; + f = f1; + f2(1); + trace; + + ` + + testScript(SCRIPT, asciiString("f(1)f1"), t) +} + +func TestFunctionToString(t *testing.T) { + const SCRIPT = ` + + Function("arg1", "arg2", "return 42").toString(); + ` + + testScript(SCRIPT, asciiString("function anonymous(arg1,arg2\n) {\nreturn 42\n}"), t) +} + +func TestObjectLiteral(t *testing.T) { + const SCRIPT = ` + var getterCalled = false; + var setterCalled = false; + + var o = {get x() {getterCalled = true}, set x(_) {setterCalled = true}}; + + o.x; + o.x = 42; + + getterCalled && setterCalled; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestConst(t *testing.T) { + const SCRIPT = ` + + var v1 = true && true; + var v2 = 1/(-1 * 0); + var v3 = 1 == 2 || v1; + var v4 = true && false + v1 === true && v2 === -Infinity && v3 === v1 && v4 === false; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestConstWhile(t *testing.T) { + const SCRIPT = ` + var c = 0; + while (2 + 2 === 4) { + if (++c > 9) { + break; + } + } + c === 10; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestConstWhileThrow(t *testing.T) { + const SCRIPT = ` + var thrown = false; + try { + while ('s' in true) { + break; + } + } catch (e) { + thrown = e instanceof TypeError + } + thrown; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestDupParams(t *testing.T) { + const SCRIPT = ` + function F(x, y, x) { + return x; + } + + F(1, 2); + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestUseUnsuppliedParam(t *testing.T) { + const SCRIPT = ` + function getMessage(message) { + if (message === undefined) { + message = ''; + } + message += " 123 456"; + return message; + } + + getMessage(); + ` + + testScript(SCRIPT, asciiString(" 123 456"), t) +} + +func TestForInLetWithInitializer(t *testing.T) { + const SCRIPT = `for (let x = 3 in {}) { }` + _, err := Compile("", SCRIPT, false) + if err == nil { + t.Fatal("Expected error") + } +} + +func TestForInLoop(t *testing.T) { + const SCRIPT = ` + function Proto() {} + Proto.prototype.x = 42; + var o = new Proto(); + o.y = 44; + o.x = 45; + var hasX = false; + var hasY = false; + + for (var i in o) { + switch(i) { + case "x": + if (hasX) { + throw new Error("Already has X"); + } + hasX = true; + break; + case "y": + if (hasY) { + throw new Error("Already has Y"); + } + hasY = true; + break; + } + } + + hasX && hasY; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestWhileLoopResult(t *testing.T) { + const SCRIPT = ` + while(false); + + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestEmptySwitch(t *testing.T) { + const SCRIPT = ` + switch(1){} + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestEmptyDoWhile(t *testing.T) { + const SCRIPT = ` + do {} while(false) + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestSwitch(t *testing.T) { + const SCRIPT = ` + function F(x) { + var i = 0; + switch (x) { + case 0: + i++; + case 1: + i++; + default: + i++; + case 2: + i++; + break; + case 3: + i++; + } + return i; + } + + F(0) + F(1) + F(2) + F(4); + + ` + + testScript(SCRIPT, intToValue(10), t) +} + +func TestSwitchDefFirst(t *testing.T) { + const SCRIPT = ` + function F(x) { + var i = 0; + switch (x) { + default: + i++; + case 0: + i++; + case 1: + i++; + case 2: + i++; + break; + case 3: + i++; + } + return i; + } + + F(0) + F(1) + F(2) + F(4); + + ` + + testScript(SCRIPT, intToValue(10), t) +} + +func TestSwitchResult(t *testing.T) { + const SCRIPT = ` + var x = 2; + + switch (x) { + case 0: + "zero"; + case 1: + "one"; + case 2: + "two"; + break; + case 3: + "three"; + default: + "default"; + } + ` + + testScript(SCRIPT, asciiString("two"), t) +} + +func TestSwitchResult1(t *testing.T) { + const SCRIPT = ` + var x = 0; + switch (x) { case 0: "two"; case 1: break} + ` + + testScript(SCRIPT, asciiString("two"), t) +} + +func TestSwitchResult2(t *testing.T) { + const SCRIPT = ` + 6; switch ("a") { case "a": 7; case "b": } + ` + + testScript(SCRIPT, valueInt(7), t) +} + +func TestSwitchResultJumpIntoEmptyEval(t *testing.T) { + const SCRIPT = ` + function t(x) { + return eval("switch(x) { case 1: 2; break; case 2: let x = 1; case 3: x+2; break; case 4: default: 9}"); + } + ""+t(2)+t(); + ` + + testScript(SCRIPT, asciiString("39"), t) +} + +func TestSwitchResultJumpIntoEmpty(t *testing.T) { + const SCRIPT = ` + switch(2) { case 1: 2; break; case 2: let x = 1; case 3: x+2; case 4: {let y = 2}; break; default: 9}; + ` + + testScript(SCRIPT, valueInt(3), t) +} + +func TestSwitchLexical(t *testing.T) { + const SCRIPT = ` + switch (true) { case true: let x = 1; } + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestSwitchBreakOuter(t *testing.T) { + const SCRIPT = ` + LOOP: + for (let i = 0; i < 10; i++) { + switch (i) { + case 0: + continue; + case 1: + let x = 1; + continue; + case 2: + try { + x++; + } catch (e) { + if (e instanceof ReferenceError) { + break LOOP; + } + } + throw new Error("Exception was not thrown"); + } + } + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestIfBreakResult(t *testing.T) { + const SCRIPT = ` + L: {if (true) {42;} break L;} + ` + + testScript(SCRIPT, intToValue(42), t) +} + +func TestSwitchNoMatch(t *testing.T) { + const SCRIPT = ` + var result; + var x; + switch (x) { + case 0: + result = "2"; + break; + } + + result; + + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestSwitchNoMatchNoDefault(t *testing.T) { + const SCRIPT = ` + switch (1) { + case 0: + } + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestSwitchNoMatchNoDefaultNoResult(t *testing.T) { + const SCRIPT = ` + switch (1) { + case 0: + } + 42; + ` + + testScript(SCRIPT, intToValue(42), t) +} + +func TestSwitchNoMatchNoDefaultNoResultMatch(t *testing.T) { + const SCRIPT = ` + switch (1) { + case 1: + } + 42; + ` + + testScript(SCRIPT, intToValue(42), t) +} + +func TestEmptySwitchNoResult(t *testing.T) { + const SCRIPT = ` + switch (1) {} + 42; + ` + + testScript(SCRIPT, intToValue(42), t) +} + +func TestGetOwnPropertyNames(t *testing.T) { + const SCRIPT = ` + var o = { + prop1: 42, + prop2: "test" + } + + var hasProp1 = false; + var hasProp2 = false; + + var names = Object.getOwnPropertyNames(o); + for (var i in names) { + var p = names[i]; + switch(p) { + case "prop1": + hasProp1 = true; + break; + case "prop2": + hasProp2 = true; + break; + } + } + + hasProp1 && hasProp2; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestArrayLiteral(t *testing.T) { + const SCRIPT = ` + + var f1Called = false; + var f2Called = false; + var f3Called = false; + var errorThrown = false; + + function F1() { + f1Called = true; + } + + function F2() { + f2Called = true; + } + + function F3() { + f3Called = true; + } + + + try { + var a = [F1(), x(F3()), F2()]; + } catch(e) { + if (e instanceof ReferenceError) { + errorThrown = true; + } else { + throw e; + } + } + + f1Called && !f2Called && f3Called && errorThrown && a === undefined; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestJumpOutOfReturn(t *testing.T) { + const SCRIPT = ` + function f() { + var a; + if (a == 0) { + return true; + } + } + + f(); + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestSwitchJumpOutOfReturn(t *testing.T) { + const SCRIPT = ` + function f(x) { + switch(x) { + case 0: + break; + default: + return x; + } + } + + f(0); + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestSetToReadOnlyPropertyStrictBracket(t *testing.T) { + const SCRIPT = ` + 'use strict'; + + var o = {}; + var thrown = false; + Object.defineProperty(o, "test", {value: 42, configurable: true}); + try { + o["test"] = 43; + } catch (e) { + thrown = e instanceof TypeError; + } + + thrown; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestSetToReadOnlyPropertyStrictDot(t *testing.T) { + const SCRIPT = ` + 'use strict'; + + var o = {}; + var thrown = false; + Object.defineProperty(o, "test", {value: 42, configurable: true}); + try { + o.test = 43; + } catch (e) { + thrown = e instanceof TypeError; + } + + thrown; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestDeleteNonConfigurablePropertyStrictBracket(t *testing.T) { + const SCRIPT = ` + 'use strict'; + + var o = {}; + var thrown = false; + Object.defineProperty(o, "test", {value: 42}); + try { + delete o["test"]; + } catch (e) { + thrown = e instanceof TypeError; + } + + thrown; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestDeleteNonConfigurablePropertyStrictDot(t *testing.T) { + const SCRIPT = ` + 'use strict'; + + var o = {}; + var thrown = false; + Object.defineProperty(o, "test", {value: 42}); + try { + delete o.test; + } catch (e) { + thrown = e instanceof TypeError; + } + + thrown; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestCompound1(t *testing.T) { + const SCRIPT = ` + var x = 0; + var scope = {x: 1}; + var f; + with (scope) { + f = function() { + x *= (delete scope.x, 2); + } + } + f(); + + scope.x === 2 && x === 0; + + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestCompound2(t *testing.T) { + const SCRIPT = ` + +var x; +x = "x"; +x ^= "1"; + + ` + testScript(SCRIPT, intToValue(1), t) +} + +func TestDeleteArguments(t *testing.T) { + defer func() { + if _, ok := recover().(*CompilerSyntaxError); !ok { + t.Fatal("Expected syntax error") + } + }() + const SCRIPT = ` + 'use strict'; + + function f() { + delete arguments; + } + + ` + testScript(SCRIPT, _undefined, t) +} + +func TestReturnUndefined(t *testing.T) { + const SCRIPT = ` + function f() { + return x; + } + + var thrown = false; + try { + f(); + } catch (e) { + thrown = e instanceof ReferenceError; + } + + thrown; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestForBreak(t *testing.T) { + const SCRIPT = ` + var supreme, count; + supreme = 5; + var __evaluated = eval("for(count=0;;) {if (count===supreme)break;else count++; }"); + if (__evaluated !== void 0) { + throw new Error('#1: __evaluated === 4. Actual: __evaluated ==='+ __evaluated ); + } + + ` + testScript(SCRIPT, _undefined, t) +} + +func TestLargeNumberLiteral(t *testing.T) { + const SCRIPT = ` + var x = 0x800000000000000000000; + x.toString(); + ` + testScript(SCRIPT, asciiString("9.671406556917033e+24"), t) +} + +func TestIncDelete(t *testing.T) { + const SCRIPT = ` + var o = {x: 1}; + o.x += (delete o.x, 1); + o.x; + ` + testScript(SCRIPT, intToValue(2), t) +} + +func TestCompoundAssignRefError(t *testing.T) { + const SCRIPT = ` + var thrown = false; + try { + a *= 1; + } catch (e) { + if (e instanceof ReferenceError) { + thrown = true; + } else { + throw e; + } + } + thrown; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestObjectLiteral__Proto__(t *testing.T) { + const SCRIPT = ` + var o = { + __proto__: null, + test: 42 + } + + Object.getPrototypeOf(o); + ` + + testScript(SCRIPT, _null, t) +} + +func TestEmptyCodeError(t *testing.T) { + if _, err := New().RunString(`i`); err == nil { + t.Fatal("Expected an error") + } else { + if e := err.Error(); e != "ReferenceError: i is not defined at :1:1(0)" { + t.Fatalf("Unexpected error: '%s'", e) + } + } +} + +func TestForOfArray(t *testing.T) { + const SCRIPT = ` + var array = [0, 'a', true, false, null, /* hole */, undefined, NaN]; + var i = 0; + + for (var value of array) { + assert.sameValue(value, array[i], 'element at index ' + i); + i++; + } + + assert.sameValue(i, 8, 'Visits all elements'); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestForOfReturn(t *testing.T) { + const SCRIPT = ` + var callCount = 0; + var iterationCount = 0; + var iterable = {}; + var x = { + set attr(_) { + throw new Test262Error(); + } + }; + + iterable[Symbol.iterator] = function() { + return { + next: function() { + return { done: false, value: 0 }; + }, + return: function() { + callCount += 1; + } + } + }; + + assert.throws(Test262Error, function() { + for (x.attr of iterable) { + iterationCount += 1; + } + }); + + assert.sameValue(iterationCount, 0, 'The loop body is not evaluated'); + assert.sameValue(callCount, 1, 'Iterator is closed'); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestForOfReturn1(t *testing.T) { + const SCRIPT = ` + var iterable = {}; + var iterationCount = 0; + + iterable[Symbol.iterator] = function() { + return { + next: function() { + return { done: false, value: null }; + }, + get return() { + throw new Test262Error(); + } + }; + }; + + assert.throws(Test262Error, function() { + for (var x of iterable) { + iterationCount += 1; + break; + } + }); + + assert.sameValue(iterationCount, 1, 'The loop body is evaluated'); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestForOfLet(t *testing.T) { + const SCRIPT = ` + var iterCount = 0; + function f() {} + for (var let of [23]) { + f(let); + if (let != 23) { + throw new Error(""); + } + iterCount += 1; + } + + iterCount; +` + testScript(SCRIPT, valueInt(1), t) +} + +func TestForOfLetLet(t *testing.T) { + const SCRIPT = ` + for (let let of [23]) { + } +` + _, err := Compile("", SCRIPT, false) + if err == nil { + t.Fatal("Expected error") + } +} + +func TestForHeadLet(t *testing.T) { + const SCRIPT = ` + for (let = 0; let < 2; let++); +` + testScript(SCRIPT, _undefined, t) +} + +func TestLhsLet(t *testing.T) { + const SCRIPT = ` + let = 1; + let; + ` + testScript(SCRIPT, valueInt(1), t) +} + +func TestLetPostfixASI(t *testing.T) { + const SCRIPT = ` + let + ++ + ` + _, err := Compile("", SCRIPT, false) + if err == nil { + t.Fatal("Expected error") + } +} + +func TestIteratorReturnNormal(t *testing.T) { + const SCRIPT = ` + var iterable = {}; + var iterationCount = 0; + + iterable[Symbol.iterator] = function() { + return { + next: function() { + return { done: ++iterationCount > 2, value: null }; + }, + get return() { + throw new Test262Error(); + } + }; + }; + + for (var x of iterable) { + } + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestIteratorReturnErrorNested(t *testing.T) { + const SCRIPT = ` + var returnCalled = {}; + function iter(id) { + return function() { + var count = 0; + return { + next: function () { + return { + value: null, + done: ++count > 2 + }; + }, + return: function () { + returnCalled[id] = true; + throw new Error(id); + } + }; + } + } + var iterable1 = {}; + iterable1[Symbol.iterator] = iter("1"); + var iterable2 = {}; + iterable2[Symbol.iterator] = iter("2"); + + try { + for (var i of iterable1) { + for (var j of iterable2) { + break; + } + } + throw new Error("no exception was thrown"); + } catch (e) { + if (e.message !== "2") { + throw e; + } + } + if (!returnCalled["1"]) { + throw new Error("no return 1"); + } + if (!returnCalled["2"]) { + throw new Error("no return 2"); + } + ` + testScript(SCRIPT, _undefined, t) +} + +func TestReturnFromForInLoop(t *testing.T) { + const SCRIPT = ` + (function f() { + for (var i in {a: 1}) { + return true; + } + })(); + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestReturnFromForOfLoop(t *testing.T) { + const SCRIPT = ` + (function f() { + for (var i of [1]) { + return true; + } + })(); + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestIfStackLeaks(t *testing.T) { + const SCRIPT = ` + var t = 0; + if (t === 0) { + t; + } + ` + testScript(SCRIPT, _positiveZero, t) +} + +func TestWithCallee(t *testing.T) { + const SCRIPT = ` + function O() { + var that = this; + this.m = function() { + return this === that; + } + } + with(new O()) { + m(); + } + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestWithScope(t *testing.T) { + const SCRIPT = ` + function f(o) { + var x = 42; + + function innerf(o) { + with (o) { + return x; + } + } + + return innerf(o); + } + f({}); + ` + testScript(SCRIPT, valueInt(42), t) +} + +func TestEvalCallee(t *testing.T) { + const SCRIPT = ` + (function () { + 'use strict'; + var v = function() { + return this === undefined; + }; + return eval('v()'); + })(); + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestEvalBindingDeleteVar(t *testing.T) { + const SCRIPT = ` + (function () { + eval("var x = 1"); + return x === 1 && delete x; + })(); + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestEvalBindingDeleteFunc(t *testing.T) { + const SCRIPT = ` + (function () { + eval("function x(){}"); + return typeof x === "function" && delete x; + })(); + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestDeleteGlobalLexical(t *testing.T) { + const SCRIPT = ` + let x; + delete x; + ` + testScript(SCRIPT, valueFalse, t) +} + +func TestDeleteGlobalEval(t *testing.T) { + const SCRIPT = ` + eval("var x"); + delete x; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestTryResultEmpty(t *testing.T) { + const SCRIPT = ` + 1; try { } finally { } + ` + testScript(SCRIPT, _undefined, t) +} + +func TestTryResultEmptyCatch(t *testing.T) { + const SCRIPT = ` + 1; try { throw null } catch(e) { } + ` + testScript(SCRIPT, _undefined, t) +} + +func TestTryResultEmptyContinueLoop(t *testing.T) { + const SCRIPT = ` + for (var i = 0; i < 2; i++) { try {throw null;} catch(e) {continue;} 'bad'} + ` + testScript(SCRIPT, _undefined, t) +} + +func TestTryEmptyCatchStackLeak(t *testing.T) { + const SCRIPT = ` + (function() { + var f; + // Make sure the outer function is not stashless. + (function() { + f++; + })(); + try { + throw new Error(); + } catch(e) {} + })(); + ` + testScript(SCRIPT, _undefined, t) +} + +func TestTryThrowEmptyCatch(t *testing.T) { + const SCRIPT = ` + try { + throw new Error(); + } + catch (e) {} + ` + testScript(SCRIPT, _undefined, t) +} + +func TestFalsyLoopBreak(t *testing.T) { + const SCRIPT = ` + while(false) { + break; + } + for(;false;) { + break; + } + undefined; + ` + MustCompile("", SCRIPT, false) +} + +func TestFalsyLoopBreakWithResult(t *testing.T) { + const SCRIPT = ` + while(false) { + break; + } + ` + testScript(SCRIPT, _undefined, t) +} + +func TestDummyCompile(t *testing.T) { + const SCRIPT = ` + 'use strict'; + + for (;false;) { + eval = 1; + } + ` + + _, err := Compile("", SCRIPT, false) + if err == nil { + t.Fatal("expected error") + } +} + +func TestDummyCompileForUpdate(t *testing.T) { + const SCRIPT = ` + 'use strict'; + + for (;false;eval=1) { + } + ` + + _, err := Compile("", SCRIPT, false) + if err == nil { + t.Fatal("expected error") + } +} + +func TestObjectLiteralWithNumericKeys(t *testing.T) { + const SCRIPT = ` + var o = {1e3: true}; + var keys = Object.keys(o); + var o1 = {get 1e3() {return true;}}; + var keys1 = Object.keys(o1); + var o2 = {1e21: true}; + var keys2 = Object.keys(o2); + let o3 = {0(){return true}}; + keys.length === 1 && keys[0] === "1000" && + keys1.length === 1 && keys1[0] === "1000" && o1[1e3] === true && + keys2.length === 1 && keys2[0] === "1e+21" && o3[0](); + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestEscapedObjectPropertyKeys(t *testing.T) { + const SCRIPT = ` + var obj = { + w\u0069th: 42 + }; + var obj = { + with() {42} + }; + ` + + _, err := Compile("", SCRIPT, false) + if err != nil { + t.Fatal(err) + } +} + +func TestEscapedKeywords(t *testing.T) { + const SCRIPT = `r\u0065turn;` + _, err := Compile("", SCRIPT, false) + if err == nil { + t.Fatal("Expected error") + } +} + +func TestEscapedLet(t *testing.T) { + const SCRIPT = ` +this.let = 0; + +l\u0065t // ASI +a; + +// If the parser treated the previous escaped "let" as a lexical declaration, +// this variable declaration will result an early syntax error. +var a; +` + _, err := Compile("", SCRIPT, false) + if err != nil { + t.Fatal(err) + } +} + +func TestObjectLiteralFuncProps(t *testing.T) { + const SCRIPT = ` + (function() { + 'use strict'; + var o = { + eval: function() {return 1;}, + arguments() {return 2;}, + test: function test1() {} + } + assert.sameValue(o.eval.name, "eval"); + assert.sameValue(o.arguments.name, "arguments"); + assert.sameValue(o.eval(), 1); + assert.sameValue(o.arguments(), 2); + assert.sameValue(o.test.name, "test1"); + })(); + ` + + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestFuncName(t *testing.T) { + const SCRIPT = ` + var method = 1; + var o = { + method: function() { + return method; + }, + method1: function method() { + return method; + } + } + o.method() === 1 && o.method1() === o.method1; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestFuncNameAssign(t *testing.T) { + const SCRIPT = ` + var f = function() {}; + var f1; + f1 = function() {}; + let f2 = function() {}; + + f.name === "f" && f1.name === "f1" && f2.name === "f2"; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestLexicalDeclGlobal(t *testing.T) { + const SCRIPT = ` + if (true) { + let it = "be"; + if (it !== "be") { + throw new Error(it); + } + } + let thrown = false; + try { + it; + } catch(e) { + if (e instanceof ReferenceError) { + thrown = true; + } + } + thrown; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestLexicalDeclFunction(t *testing.T) { + const SCRIPT = ` + function f() { + if (true) { + let it = "be"; + if (it !== "be") { + throw new Error(it); + } + } + let thrown = false; + try { + it; + } catch(e) { + if (e instanceof ReferenceError) { + thrown = true; + } + } + return thrown; + } + f(); + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestLexicalDynamicScope(t *testing.T) { + const SCRIPT = ` + const global = 1; + function f() { + const func = global + 1; + function inner() { + function assertThrows(fn) { + let thrown = false; + try { + fn(); + } catch (e) { + if (e instanceof TypeError) { + thrown = true; + } else { + throw e; + } + } + if (!thrown) { + throw new Error("Did not throw"); + } + } + + assertThrows(function() { + func++; + }); + assertThrows(function() { + global++; + }); + + assertThrows(function() { + eval("func++"); + }); + assertThrows(function() { + eval("global++"); + }); + + return eval("func + 1"); + } + return inner(); + } + f(); + ` + testScript(SCRIPT, valueInt(3), t) +} + +func TestLexicalDynamicScope1(t *testing.T) { + const SCRIPT = ` + (function() { + const x = 1 * 4; + return (function() { + eval(""); + return x; + })(); + })(); + ` + testScript(SCRIPT, intToValue(4), t) +} + +func TestLexicalDynamicScope2(t *testing.T) { + const SCRIPT = ` + (function() { + const x = 1 + 3; + var y = 2 * 2; + eval(""); + return x; + })(); + ` + testScript(SCRIPT, intToValue(4), t) +} + +func TestNonStrictLet(t *testing.T) { + const SCRIPT = ` + var let = 1; + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestStrictLet(t *testing.T) { + const SCRIPT = ` + var let = 1; + ` + + _, err := Compile("", SCRIPT, true) + if err == nil { + t.Fatal("Expected an error") + } +} + +func TestLetLet(t *testing.T) { + const SCRIPT = ` + let let = 1; + ` + + _, err := Compile("", SCRIPT, false) + if err == nil { + t.Fatal("Expected an error") + } +} + +func TestLetASI(t *testing.T) { + const SCRIPT = ` + while (false) let // ASI + x = 1; + ` + + _, err := Compile("", SCRIPT, false) + if err != nil { + t.Fatal(err) + } +} + +func TestLetASI1(t *testing.T) { + const SCRIPT = ` + let + x = 1; + ` + + _, err := Compile("", SCRIPT, true) + if err != nil { + t.Fatal(err) + } +} + +func TestLetNoASI(t *testing.T) { + const SCRIPT = ` + function f() {}let +x = 1; + ` + + _, err := Compile("", SCRIPT, true) + if err != nil { + t.Fatal(err) + } +} + +func TestLetNoASI1(t *testing.T) { + const SCRIPT = ` +let +let = 1; + ` + + _, err := Compile("", SCRIPT, false) + if err == nil { + t.Fatal("Expected error") + } +} + +func TestLetArrayWithNewline(t *testing.T) { + const SCRIPT = ` + with ({}) let + [a] = 0; + ` + + _, err := Compile("", SCRIPT, false) + if err == nil { + t.Fatal("Expected error") + } +} + +func TestDynamicUninitedVarAccess(t *testing.T) { + const SCRIPT = ` + function f() { + var x; + return eval("x"); + } + f(); + ` + testScript(SCRIPT, _undefined, t) +} + +func TestLexicalForLoopNoClosure(t *testing.T) { + const SCRIPT = ` + let sum = 0; + for (let i = 0; i < 3; i++) { + sum += i; + } + sum; + ` + testScript(SCRIPT, valueInt(3), t) +} + +func TestLexicalForLoopClosure(t *testing.T) { + const SCRIPT = ` + var f = []; + for (let i = 0; i < 3; i++) { + f.push(function() { + return i; + }); + } + f.length === 3 && f[0]() === 0 && f[1]() === 1 && f[2]() === 2; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestLexicalForLoopClosureInNext(t *testing.T) { + const SCRIPT = ` + const a = []; + for (let i = 0; i < 5; a.push(function () { return i; }), ++i) { } + let res = ""; + for (let k = 0; k < 5; ++k) { + res += ""+a[k](); + } + res; + ` + testScript(SCRIPT, asciiString("12345"), t) +} + +func TestVarForLoop(t *testing.T) { + const SCRIPT = ` + var f = []; + for (var i = 0, j = 0; i < 3; i++) { + f.push(function() { + return i; + }); + } + f.length === 3 && f[0]() === 3 && f[1]() === 3 && f[2]() === 3; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestLexicalForOfLoop(t *testing.T) { + const SCRIPT = ` + var f = []; + for (let i of [0, 1, 2]) { + f.push(function() { + return i; + }); + } + f.length === 3 && f[0]() === 0 && f[1]() === 1 && f[2]() === 2; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestLexicalForOfLoopContBreak(t *testing.T) { + const SCRIPT = ` + const f = []; + for (let i of [0, 1, 2, 3, 4, 5]) { + if (i % 2) continue; + f.push(function() { + return i; + }); + if (i > 2) break; + } + let res = ""; + f.forEach(function(item) {res += item()}); + f.length === 3 && res === "024"; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestVarBlockConflict(t *testing.T) { + const SCRIPT = ` + let x; + { + if (false) { + var x; + } + } + ` + _, err := Compile("", SCRIPT, false) + if err == nil { + t.Fatal("Expected an error") + } +} + +func TestVarBlockConflictEval(t *testing.T) { + const SCRIPT = ` + assert.throws(SyntaxError, function() { + let x; + { + if (true) { + eval("var x"); + } + } + }); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestVarBlockNoConflict(t *testing.T) { + const SCRIPT = ` + function f() { + let x; + function ff() { + { + var x = 3; + } + } + ff(); + } + f(); + ` + testScript(SCRIPT, _undefined, t) +} + +func TestVarBlockNoConflictEval(t *testing.T) { + const SCRIPT = ` + function f() { + let x; + function ff() { + { + eval("var x = 3"); + } + } + ff(); + } + f(); + ` + testScript(SCRIPT, _undefined, t) +} + +func TestVarDeclCorrectScope(t *testing.T) { + const SCRIPT = ` + function f() { + { + let z; + eval("var x = 3"); + } + return x; + } + f(); + ` + testScript(SCRIPT, valueInt(3), t) +} + +func TestLexicalCatch(t *testing.T) { + const SCRIPT = ` + try { + throw null; + } catch (e) { + let x = 1; + function f() {} + e; + } + ` + testScript(SCRIPT, _null, t) +} + +func TestArgumentsLexicalDecl(t *testing.T) { + const SCRIPT = ` + function f1() { + let arguments; + return arguments; + } + f1(42); + ` + testScript(SCRIPT, _undefined, t) +} + +func TestArgumentsLexicalDeclAssign(t *testing.T) { + const SCRIPT = ` + function f1() { + let arguments = arguments; + return a; + } + assert.throws(ReferenceError, function() { + f1(42); + }); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestLexicalConstModifyFromEval(t *testing.T) { + const SCRIPT = ` + const x = 1; + function f() { + eval("x = 2"); + } + assert.throws(TypeError, function() { + f(); + }); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestLexicalStrictNames(t *testing.T) { + const SCRIPT = `let eval = 1;` + + _, err := Compile("", SCRIPT, true) + if err == nil { + t.Fatal("Expected an error") + } +} + +func TestAssignAfterStackExpand(t *testing.T) { + // make sure the reference to the variable x does not remain stale after the stack is copied + const SCRIPT = ` + function f() { + let sum = 0; + for (let i = 0; i < arguments.length; i++) { + sum += arguments[i]; + } + return sum; + } + function testAssignment() { + var x = 0; + var scope = {}; + + with (scope) { + x = (scope.x = f(0, 0, 0, 0, 0, 0, 1, 1), 1); + } + + if (scope.x !== 2) { + throw new Error('#1: scope.x === 2. Actual: ' + (scope.x)); + } + if (x !== 1) { + throw new Error('#2: x === 1. Actual: ' + (x)); + } + } + testAssignment(); + ` + testScript(SCRIPT, _undefined, t) +} + +func TestArgAccessFromDynamicStash(t *testing.T) { + const SCRIPT = ` + function f(arg) { + function test() { + eval(""); + return a; + } + return arg; + } + f(true); + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestLoadMixedLex(t *testing.T) { + const SCRIPT = ` + function f() { + let a = 1; + { + function inner() { + eval("var a = true"); + return a; + } + return inner(); + } + } + f(); + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestObjectLiteralSpread(t *testing.T) { + const SCRIPT = ` + let src = {prop1: 1}; + Object.defineProperty(src, "prop2", {value: 2, configurable: true}); + Object.defineProperty(src, "prop3", {value: 3, enumerable: true, configurable: true}); + let target = {prop4: 4, ...src}; + assert(deepEqual(target, {prop1: 1, prop3: 3, prop4: 4})); + ` + testScriptWithTestLibX(SCRIPT, _undefined, t) +} + +func TestArrayLiteralSpread(t *testing.T) { + const SCRIPT = ` + let a1 = [1, 2]; + let a2 = [3, 4]; + let a = [...a1, 0, ...a2, 1]; + assert(compareArray(a, [1, 2, 0, 3, 4, 1])); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestObjectAssignmentPattern(t *testing.T) { + const SCRIPT = ` + let a, b, c; + ({a, b, c=3} = {a: 1, b: 2}); + assert.sameValue(a, 1, "a"); + assert.sameValue(b, 2, "b"); + assert.sameValue(c, 3, "c"); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestObjectAssignmentPatternNoDyn(t *testing.T) { + const SCRIPT = ` + (function() { + let a, b, c; + ({a, b, c=3} = {a: 1, b: 2}); + assert.sameValue(a, 1, "a"); + assert.sameValue(b, 2, "b"); + assert.sameValue(c, 3, "c"); + })(); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestObjectAssignmentPatternNested(t *testing.T) { + const SCRIPT = ` + let a, b, c, d; + ({a, b, c: {d} = 3} = {a: 1, b: 2, c: {d: 4}}); + assert.sameValue(a, 1, "a"); + assert.sameValue(b, 2, "b"); + assert.sameValue(c, undefined, "c"); + assert.sameValue(d, 4, "d"); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestObjectAssignmentPatternEvalOrder(t *testing.T) { + const SCRIPT = ` + let trace = ""; + let target_obj = {}; + + function src() { + trace += "src(),"; + return { + get a() { + trace += "get a,"; + return "a"; + } + } + } + + function prop1() { + trace += "prop1()," + return { + toString: function() { + trace += "prop1-to-string(),"; + return "a"; + } + } + } + + function prop2() { + trace += "prop2(),"; + return { + toString: function() { + trace += "prop2-to-string(),"; + return "b"; + } + } + } + + function target() { + trace += "target()," + return target_obj; + } + + let a, b; + + ({[prop1()]: target().a, [prop2()]: b} = src()); + if (target_obj.a !== "a") { + throw new Error("target_obj.a="+target_obj.a); + } + trace; + ` + testScript(SCRIPT, asciiString("src(),prop1(),prop1-to-string(),target(),get a,prop2(),prop2-to-string(),"), t) +} + +func TestArrayAssignmentPatternEvalOrder(t *testing.T) { + const SCRIPT = ` + let trace = ""; + + let src_arr = { + [Symbol.iterator]: function() { + let done = false; + return { + next: function() { + trace += "next,"; + if (!done) { + done = true; + return {value: 0}; + } + return {done: true}; + }, + return: function() { + trace += "return,"; + } + } + } + } + + function src() { + trace += "src(),"; + return src_arr; + } + + let tgt = { + get a() { + trace += "get a,"; + return "a"; + }, + get b() { + trace += "get b,"; + return "b"; + } + } + + function target() { + trace += "target(),"; + return tgt; + } + + function default_a() { + trace += "default a,"; + return "def_a"; + } + + function default_b() { + trace += "default b,"; + return "def_b"; + } + + ([target().a = default_a(), target().b = default_b()] = src()); + trace; + ` + testScript(SCRIPT, asciiString("src(),target(),next,target(),next,default b,"), t) +} + +func TestObjectAssignPatternRest(t *testing.T) { + const SCRIPT = ` + let a, b, c, d; + ({a, b, c, ...d} = {a: 1, b: 2, d: 4}); + assert.sameValue(a, 1, "a"); + assert.sameValue(b, 2, "b"); + assert.sameValue(c, undefined, "c"); + assert(deepEqual(d, {d: 4}), "d"); + ` + testScriptWithTestLibX(SCRIPT, _undefined, t) +} + +func TestObjectBindPattern(t *testing.T) { + const SCRIPT = ` + let {a, b, c, ...d} = {a: 1, b: 2, d: 4}; + assert.sameValue(a, 1, "a"); + assert.sameValue(b, 2, "b"); + assert.sameValue(c, undefined, "c"); + assert(deepEqual(d, {d: 4}), "d"); + + var { x: y, } = { x: 23 }; + + assert.sameValue(y, 23); + + assert.throws(ReferenceError, function() { + x; + }); + ` + testScriptWithTestLibX(SCRIPT, _undefined, t) +} + +func TestObjLiteralShorthandWithInitializer(t *testing.T) { + const SCRIPT = ` + o = {a=1}; + ` + _, err := Compile("", SCRIPT, false) + if err == nil { + t.Fatal("Expected an error") + } +} + +func TestObjLiteralShorthandLetStringLit(t *testing.T) { + const SCRIPT = ` + o = {"let"}; + ` + _, err := Compile("", SCRIPT, false) + if err == nil { + t.Fatal("Expected an error") + } +} + +func TestObjLiteralComputedKeys(t *testing.T) { + const SCRIPT = ` + let o = { + get [Symbol.toString]() { + } + } + ` + testScript(SCRIPT, _undefined, t) +} + +func TestObjLiteralComputedKeysEvalOrder(t *testing.T) { + const SCRIPT = ` + let trace = []; + function key() { + trace.push("key"); + return { + toString: function() { + trace.push("key-toString"); + return "key"; + } + } + } + function val() { + trace.push("val"); + return "val"; + } + + const _ = { + [key()]: val(), + } + + trace.join(","); + ` + testScript(SCRIPT, asciiString("key,key-toString,val"), t) +} + +func TestArrayAssignPattern(t *testing.T) { + const SCRIPT = ` + let a, b; + ([a, b] = [1, 2]); + a === 1 && b === 2; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestArrayAssignPattern1(t *testing.T) { + const SCRIPT = ` + let a, b; + ([a = 3, b = 2] = [1]); + a === 1 && b === 2; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestArrayAssignPatternLHS(t *testing.T) { + const SCRIPT = ` + let a = {}; + [ a.b, a['c'] = 2 ] = [1]; + a.b === 1 && a.c === 2; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestArrayAssignPatternElision(t *testing.T) { + const SCRIPT = ` + let a, b; + ([a,, b] = [1, 4, 2]); + a === 1 && b === 2; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestArrayAssignPatternRestPattern(t *testing.T) { + const SCRIPT = ` + let a, b, z; + [ z, ...[a, b] ] = [0, 1, 2]; + z === 0 && a === 1 && b === 2; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestArrayBindingPattern(t *testing.T) { + const SCRIPT = ` + let [a, b] = [1, 2]; + a === 1 && b === 2; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestObjectPatternShorthandInit(t *testing.T) { + const SCRIPT = ` + [...{ x = 1 }] = []; + x; + ` + testScript(SCRIPT, valueInt(1), t) +} + +func TestArrayBindingPatternRestPattern(t *testing.T) { + const SCRIPT = ` + const [a, b, ...[c, d]] = [1, 2, 3, 4]; + a === 1 && b === 2 && c === 3 && d === 4; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestForVarPattern(t *testing.T) { + const SCRIPT = ` + var o = {a: 1}; + var trace = ""; + for (var [key, value] of Object.entries(o)) { + trace += key+":"+value; + } + trace; + ` + testScript(SCRIPT, asciiString("a:1"), t) +} + +func TestForLexPattern(t *testing.T) { + const SCRIPT = ` + var o = {a: 1}; + var trace = ""; + for (const [key, value] of Object.entries(o)) { + trace += key+":"+value; + } + trace; + ` + testScript(SCRIPT, asciiString("a:1"), t) +} + +func TestBindingPatternRestTrailingComma(t *testing.T) { + const SCRIPT = ` + const [a, b, ...rest,] = []; + ` + _, err := Compile("", SCRIPT, false) + if err == nil { + t.Fatal("Expected an error") + } +} + +func TestAssignPatternRestTrailingComma(t *testing.T) { + const SCRIPT = ` + ([a, b, ...rest,] = []); + ` + _, err := Compile("", SCRIPT, false) + if err == nil { + t.Fatal("Expected an error") + } +} + +func TestFuncParamInitializerSimple(t *testing.T) { + const SCRIPT = ` + function f(a = 1) { + return a; + } + ""+f()+f(2); + ` + testScript(SCRIPT, asciiString("12"), t) +} + +func TestFuncParamObjectPatternSimple(t *testing.T) { + const SCRIPT = ` + function f({a, b} = {a: 1, b: 2}) { + return "" + a + b; + } + ""+f()+" "+f({a: 3, b: 4}); + ` + testScript(SCRIPT, asciiString("12 34"), t) +} + +func TestFuncParamRestStackSimple(t *testing.T) { + const SCRIPT = ` + function f(arg1, ...rest) { + return rest; + } + let ar = f(1, 2, 3); + ar.join(","); + ` + testScript(SCRIPT, asciiString("2,3"), t) +} + +func TestFuncParamRestStashSimple(t *testing.T) { + const SCRIPT = ` + function f(arg1, ...rest) { + eval("true"); + return rest; + } + let ar = f(1, 2, 3); + ar.join(","); + ` + testScript(SCRIPT, asciiString("2,3"), t) +} + +func TestRestArgsNotInStash(t *testing.T) { + const SCRIPT = ` + function f(...rest) { + () => rest; + return rest.length; + } + f(1,2); + ` + testScript(SCRIPT, valueInt(2), t) +} + +func TestRestArgsInStash(t *testing.T) { + const SCRIPT = ` + function f(first, ...rest) { + () => first; + () => rest; + return rest.length; + } + f(1,2); + ` + testScript(SCRIPT, valueInt(1), t) +} + +func TestRestArgsInStashFwdRef(t *testing.T) { + const SCRIPT = ` + function f(first = eval(), ...rest) { + () => first; + () => rest; + return rest.length === 1 && rest[0] === 2; + } + f(1,2); + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestFuncParamRestPattern(t *testing.T) { + const SCRIPT = ` + function f(arg1, ...{0: rest1, 1: rest2}) { + return ""+arg1+" "+rest1+" "+rest2; + } + f(1, 2, 3); + ` + testScript(SCRIPT, asciiString("1 2 3"), t) +} + +func TestFuncParamForwardRef(t *testing.T) { + const SCRIPT = ` + function f(a = b + 1, b) { + return ""+a+" "+b; + } + f(1, 2); + ` + testScript(SCRIPT, asciiString("1 2"), t) +} + +func TestFuncParamForwardRefMissing(t *testing.T) { + const SCRIPT = ` + function f(a = b + 1, b) { + return ""+a+" "+b; + } + assert.throws(ReferenceError, function() { + f(); + }); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestFuncParamInnerRef(t *testing.T) { + const SCRIPT = ` + function f(a = inner) { + var inner = 42; + return a; + } + assert.throws(ReferenceError, function() { + f(); + }); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestFuncParamInnerRefEval(t *testing.T) { + const SCRIPT = ` + function f(a = eval("inner")) { + var inner = 42; + return a; + } + assert.throws(ReferenceError, function() { + f(); + }); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestFuncParamCalleeName(t *testing.T) { + const SCRIPT = ` + function f(a = f) { + var f; + return f; + } + typeof f(); + ` + testScript(SCRIPT, asciiString("undefined"), t) +} + +func TestFuncParamVarCopy(t *testing.T) { + const SCRIPT = ` + function f(a = f) { + var a; + return a; + } + typeof f(); + ` + testScript(SCRIPT, asciiString("function"), t) +} + +func TestFuncParamScope(t *testing.T) { + const SCRIPT = ` + var x = 'outside'; + var probe1, probe2; + + function f( + _ = probe1 = function() { return x; }, + __ = (eval('var x = "inside";'), probe2 = function() { return x; }) + ) { + } + f(); + probe1()+" "+probe2(); + ` + testScript(SCRIPT, asciiString("inside inside"), t) +} + +func TestDefParamsStackPtr(t *testing.T) { + const SCRIPT = ` + function A() {}; + A.B = function () {}; + function D(message = '') { + var C = A.B; + C([1,2,3]); + }; + + D(); + ` + testScript(SCRIPT, _undefined, t) +} + +func TestNestedVariadicCalls(t *testing.T) { + const SCRIPT = ` + function f() { + return Array.prototype.join.call(arguments, ","); + } + f(...[1], "a", f(...[2])); + ` + testScript(SCRIPT, asciiString("1,a,2"), t) +} + +func TestVariadicNew(t *testing.T) { + const SCRIPT = ` + function C() { + this.res = Array.prototype.join.call(arguments, ","); + } + var c = new C(...[1], "a", new C(...[2]).res); + c.res; + ` + testScript(SCRIPT, asciiString("1,a,2"), t) +} + +func TestVariadicUseStackVars(t *testing.T) { + const SCRIPT = ` + function A(message) { return message; } + function B(...args){ + return A(...args); + } + B("C"); + ` + testScript(SCRIPT, asciiString("C"), t) +} + +func TestCatchParamPattern(t *testing.T) { + const SCRIPT = ` + function f() { + let x = 3; + try { + throw {a: 1, b: 2}; + } catch ({a, b, c = x}) { + let x = 99; + return ""+a+" "+b+" "+c; + } + } + f(); + ` + testScript(SCRIPT, asciiString("1 2 3"), t) +} + +func TestArrowUseStrict(t *testing.T) { + // simple parameter list -- ok + _, err := Compile("", "(a) => {'use strict';}", false) + if err != nil { + t.Fatal(err) + } + // non-simple parameter list -- syntax error + _, err = Compile("", "(a=0) => {'use strict';}", false) + if err == nil { + t.Fatal("expected error") + } +} + +func TestArrowBoxedThis(t *testing.T) { + const SCRIPT = ` + var context; + fn = function() { + return (arg) => { var local; context = this; }; + }; + + fn()(); + context === this; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestParameterOverride(t *testing.T) { + const SCRIPT = ` + function f(arg) { + var arg = arg || "default" + return arg + } + f() + ` + testScript(SCRIPT, asciiString("default"), t) +} + +func TestEvalInIterScope(t *testing.T) { + const SCRIPT = ` + for (let a = 0; a < 1; a++) { + eval("a"); + } + ` + + testScript(SCRIPT, valueInt(0), t) +} + +func TestTemplateLiterals(t *testing.T) { + vm := New() + _, err := vm.RunString("const a = 1, b = 'b';") + if err != nil { + t.Fatal(err) + } + f := func(t *testing.T, template, expected string) { + res, err := vm.RunString(template) + if err != nil { + t.Fatal(err) + } + if actual := res.Export(); actual != expected { + t.Fatalf("Expected: %q, actual: %q", expected, actual) + } + } + t.Run("empty", func(t *testing.T) { + f(t, "``", "") + }) + t.Run("noSub", func(t *testing.T) { + f(t, "`test`", "test") + }) + t.Run("emptyTail", func(t *testing.T) { + f(t, "`a=${a},b=${b}`", "a=1,b=b") + }) + t.Run("emptyHead", func(t *testing.T) { + f(t, "`${a},b=${b}$`", "1,b=b$") + }) + t.Run("headAndTail", func(t *testing.T) { + f(t, "`a=${a},b=${b}$`", "a=1,b=b$") + }) +} + +func TestTaggedTemplate(t *testing.T) { + const SCRIPT = ` + let res; + const o = { + tmpl() { + res = this; + return () => {}; + } + } + ` + + "o.tmpl()`test`;" + ` + res === o; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestDuplicateGlobalFunc(t *testing.T) { + const SCRIPT = ` + function a(){} + function b(){ return "b" } + function c(){ return "c" } + function a(){} + b(); + ` + + testScript(SCRIPT, asciiString("b"), t) +} + +func TestDuplicateFunc(t *testing.T) { + const SCRIPT = ` + function f() { + function a(){} + function b(){ return "b" } + function c(){ return "c" } + function a(){} + return b(); + } + f(); + ` + + testScript(SCRIPT, asciiString("b"), t) +} + +func TestSrcLocations(t *testing.T) { + // Do not reformat, assertions depend on the line and column numbers + const SCRIPT = ` + let i = { + valueOf() { + throw new Error(); + } + }; + try { + i++; + } catch(e) { + assertStack(e, [["test.js", "valueOf", 4, 10], + ["test.js", "", 8, 3] + ]); + } + + Object.defineProperty(globalThis, "x", { + get() { + throw new Error(); + }, + set() { + throw new Error(); + } + }); + + try { + x; + } catch(e) { + assertStack(e, [["test.js", "get", 17, 10], + ["test.js", "", 25, 3] + ]); + } + + try { + x++; + } catch(e) { + assertStack(e, [["test.js", "get", 17, 10], + ["test.js", "", 33, 3] + ]); + } + + try { + x = 2; + } catch(e) { + assertStack(e, [["test.js", "set", 20, 10], + ["test.js", "", 41, 3] + ]); + } + + try { + +i; + } catch(e) { + assertStack(e, [["test.js", "valueOf", 4, 10], + ["test.js", "", 49, 4] + ]); + } + + try { + let n; + n.field = { + "key1": "test", + "key2": {}, + } + } catch(e) { + assertStack(e, [["test.js", "", 58, 5] + ]); + } + ` + testScriptWithTestLibX(SCRIPT, _undefined, t) +} + +func TestSrcLocationThrowLiteral(t *testing.T) { + vm := New() + _, err := vm.RunString(` + const z = 1; + throw ""; + `) + if ex, ok := err.(*Exception); ok { + pos := ex.stack[0].Position() + if pos.Line != 3 { + t.Fatal(pos) + } + } else { + t.Fatal(err) + } +} + +func TestSrcLocation(t *testing.T) { + prg := MustCompile("test.js", ` +f(); +var x = 1; +let y = 1; +let [z1, z2] = [0, 0]; + +var [z3, z4] = [0, 0]; + `, false) + const ( + varLine = 3 + letLine = 4 + dstrLetLine = 5 + dstrVarLine = 7 + ) + linesOfInterest := map[int]string{ + varLine: "var", + letLine: "let", + dstrLetLine: "destruct let", + dstrVarLine: "destruct var", + } + for i := range prg.code { + loc := prg.src.Position(prg.sourceOffset(i)) + delete(linesOfInterest, loc.Line) + if len(linesOfInterest) == 0 { + break + } + } + for _, v := range linesOfInterest { + t.Fatalf("no %s line", v) + } +} + +func TestBadObjectKey(t *testing.T) { + _, err := Compile("", "({!:0})", false) + if err == nil { + t.Fatal("expected error") + } +} + +func TestConstantFolding(t *testing.T) { + testValues := func(prg *Program, result Value, t *testing.T) { + values := make(map[unistring.String]struct{}) + for _, ins := range prg.code { + if lv, ok := ins.(loadVal); ok { + values[lv.v.string()] = struct{}{} + } + } + if len(values) != 1 { + prg.dumpCode(t.Logf) + t.Fatalf("values: %v", values) + } + } + f := func(src string, result Value, t *testing.T) { + prg := MustCompile("test.js", src, false) + testValues(prg, result, t) + New().testPrg(prg, result, t) + } + ff := func(src string, result Value, t *testing.T) { + prg := MustCompile("test.js", src, false) + fl := prg.code[0].(*newFunc) + testValues(fl.prg, result, t) + New().testPrg(prg, result, t) + } + + t.Run("lexical binding", func(t *testing.T) { + f("const x = 1 + 2; x", valueInt(3), t) + }) + t.Run("var binding", func(t *testing.T) { + f("var x = 1 + 2; x", valueInt(3), t) + }) + t.Run("assignment", func(t *testing.T) { + f("x = 1 + 2; x", valueInt(3), t) + }) + t.Run("object pattern", func(t *testing.T) { + f("const {x = 1 + 2} = {}; x", valueInt(3), t) + }) + t.Run("array pattern", func(t *testing.T) { + f("const [x = 1 + 2] = []; x", valueInt(3), t) + }) + t.Run("object literal", func(t *testing.T) { + f("var o = {x: 1 + 2}; o.x", valueInt(3), t) + }) + t.Run("array literal", func(t *testing.T) { + f("var a = [3, 3, 3, 1 + 2]; a[3]", valueInt(3), t) + }) + t.Run("default function parameter", func(t *testing.T) { + ff("function f(arg = 1 + 2) {return arg}; f()", valueInt(3), t) + }) + t.Run("return", func(t *testing.T) { + ff("function f() {return 1 + 2}; f()", valueInt(3), t) + }) +} + +func TestStringInterning(t *testing.T) { + const SCRIPT = ` + const str1 = "Test"; + function f() { + return "Test"; + } + [str1, f()]; + ` + vm := New() + res, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + str1 := res.(*Object).Get("0").String() + str2 := res.(*Object).Get("1").String() + if unsafe.StringData(str1) != unsafe.StringData(str2) { + t.Fatal("not interned") + } +} + +func TestAssignBeforeInit(t *testing.T) { + const SCRIPT = ` + assert.throws(ReferenceError, () => { + a = 1; + let a; + }); + + assert.throws(ReferenceError, () => { + ({a, b} = {a: 1, b: 2}); + let a, b; + }); + + assert.throws(ReferenceError, () => { + (function() { + eval(""); + ({a} = {a: 1}); + })(); + let a; + }); + + assert.throws(ReferenceError, () => { + const ctx = {x: 1}; + function t() { + delete ctx.x; + return 42; + } + with(ctx) { + (function() { + 'use strict'; + ({x} = {x: t()}); + })(); + } + return ctx.x; + }); + + assert.throws(ReferenceError, () => { + const ctx = {x: 1}; + function t() { + delete ctx.x; + return 42; + } + with(ctx) { + (function() { + 'use strict'; + const src = {}; + Object.defineProperty(src, "x", { + get() { + return t(); + } + }); + ({x} = src); + })(); + } + return ctx.x; + }); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestOptChainCallee(t *testing.T) { + const SCRIPT = ` + var a; + assert.sameValue(a?.(true), undefined); + a = null; + assert.sameValue(a?.(), undefined); + var o = {n: null}; + assert.sameValue(o.m?.(true), undefined); + assert.sameValue(o.n?.(true), undefined); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestObjectLiteralSuper(t *testing.T) { + const SCRIPT = ` + const proto = { + m() { + return 40; + } + } + const o = { + m() { + return super.m() + 2; + } + } + o.__proto__ = proto; + o.m(); + ` + testScript(SCRIPT, intToValue(42), t) +} + +func TestClassCaptureThisInFieldInit(t *testing.T) { + const SCRIPT = ` + let capture; + + class C { + a = () => this + } + + let c = new C(); + c.a() === c; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestClassUseThisInFieldInit(t *testing.T) { + const SCRIPT = ` + let capture; + + class C { + a = this + } + + let c = new C(); + c.a === c; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestClassCaptureThisInStaticFieldInit(t *testing.T) { + const SCRIPT = ` + let capture; + + class C { + static a = (capture = () => this, 0) + } + + let c = new C(); + capture() === C; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestClassDynCaptureThisInStaticFieldInit(t *testing.T) { + const SCRIPT = ` + class C { + static a = eval("this") + } + + C.a === C; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestCompileClass(t *testing.T) { + const SCRIPT = ` + class C extends Error { + a = true; + b = 1; + ["b".toUpperCase()] = 2 + static A = Math.random() < 1 + constructor(message = "My Error") { + super(message); + } + static M() { + } + static M1() { + } + m() { + //return C.a; + } + m1() { + return true; + } + static { + this.supername = super.name; + } + } + let c = new C(); + c.a === true && c.b === 1 && c.B === 2 && c.m1() && C.A && C.supername === "Error"; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestSuperInEval(t *testing.T) { + const SCRIPT = ` + class C extends Error { + constructor() { + eval("super()"); + } + m() { + return eval("super.name"); + } + } + let c = new C(); + c.m() === "Error"; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestSuperRefDot(t *testing.T) { + const SCRIPT = ` + let thisGet, thisSet; + class P { + _p = 0 + get p() { + thisGet = this; + return this._p; + } + set p(v) { + thisSet = this; + this._p = v; + } + } + + class C extends P { + g() { + return super.p; + } + s(v) { + super.p = v; + } + + inc() { + super.p++; + } + incR() { + return super.p++; + } + + inc1() { + ++super.p; + } + + inc1R() { + return ++super.p; + } + unary() { + return +super.p; + } + pattern() { + [super.p] = [9]; + } + } + + let o = new C(); + assert.sameValue(o.g(), 0, "get value"); + assert.sameValue(thisGet, o, "get this"); + o.s(1); + assert.sameValue(o._p, 1, "set value"); + assert.sameValue(thisSet, o, "set this"); + + thisGet = undefined; + thisSet = undefined; + o.inc(); + assert.sameValue(o._p, 2, "inc value"); + assert.sameValue(thisGet, o, "inc thisGet"); + assert.sameValue(thisSet, o, "inc thisSet"); + + thisGet = undefined; + thisSet = undefined; + assert.sameValue(o.incR(), 2, "incR result"); + assert.sameValue(o._p, 3, "incR value"); + assert.sameValue(thisGet, o, "incR thisGet"); + assert.sameValue(thisSet, o, "incR thisSet"); + + thisGet = undefined; + thisSet = undefined; + o.inc1(); + assert.sameValue(o._p, 4, "inc1 value"); + assert.sameValue(thisGet, o, "inc1 thisGet"); + assert.sameValue(thisSet, o, "inc1 thisSet"); + + thisGet = undefined; + thisSet = undefined; + assert.sameValue(o.inc1R(), 5, "inc1R result"); + assert.sameValue(o._p, 5, "inc1R value"); + assert.sameValue(thisGet, o, "inc1R thisGet"); + assert.sameValue(thisSet, o, "inc1R thisSet"); + + assert.sameValue(o.unary(), 5, "unary"); + + o.pattern(); + assert.sameValue(o._p, 9, "pattern"); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestPrivateRefDot(t *testing.T) { + const SCRIPT = ` + class C { + #p = 0; + g() { + return this.#p; + } + s(v) { + this.#p = v; + } + + inc() { + this.#p++; + } + incR() { + return this.#p++; + } + + inc1() { + ++this.#p; + } + + inc1R() { + return ++this.#p; + } + pattern() { + [this.#p] = [9]; + } + } + + let o = new C(); + assert.sameValue(o.g(), 0, "get value"); + o.s(1); + assert.sameValue(o.g(), 1, "set value"); + + o.inc(); + assert.sameValue(o.g(), 2, "inc value"); + + assert.sameValue(o.incR(), 2, "incR result"); + assert.sameValue(o.g(), 3, "incR value"); + + o.inc1(); + assert.sameValue(o.g(), 4, "inc1 value"); + + assert.sameValue(o.inc1R(), 5, "inc1R result"); + assert.sameValue(o.g(), 5, "inc1R value"); + + o.pattern(); + assert.sameValue(o.g(), 9, "pattern"); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestPrivateRefDotEval(t *testing.T) { + const SCRIPT = ` + class C { + #p = 0; + g() { + return eval("this.#p"); + } + s(v) { + eval("this.#p = v"); + } + + incR() { + return eval("this.#p++"); + } + + inc1R() { + return eval("++this.#p"); + } + + pattern() { + eval("[this.#p] = [9]"); + } + } + + let o = new C(); + assert.sameValue(o.g(), 0, "get value"); + o.s(1); + assert.sameValue(o.g(), 1, "set value"); + + assert.sameValue(o.incR(), 1, "incR result"); + assert.sameValue(o.g(), 2, "incR value"); + + assert.sameValue(o.inc1R(), 3, "inc1R result"); + assert.sameValue(o.g(), 3, "inc1R value"); + + o.pattern(); + assert.sameValue(o.g(), 9, "pattern"); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestSuperRefDotCallee(t *testing.T) { + const SCRIPT = ` + class P { + get p() { + return function() { + return this; + }; + } + } + + class C extends P { + m() { + return super.p(); + } + } + + let o = new C(); + o.m() === o; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestSuperRefBracket(t *testing.T) { + const SCRIPT = ` + let PROP = "p"; + let thisGet, thisSet; + class P { + _p = 0 + get p() { + thisGet = this; + return this._p; + } + set p(v) { + thisSet = this; + this._p = v; + } + } + + class C extends P { + g() { + return super[PROP]; + } + s(v) { + super[PROP] = v; + } + + inc() { + super[PROP]++; + } + incR() { + return super[PROP]++; + } + + inc1() { + ++super[PROP]; + } + + inc1R() { + return ++super[PROP]; + } + pattern() { + [super[PROP]] = [9]; + } + } + + let o = new C(); + assert.sameValue(o.g(), 0, "get value"); + assert.sameValue(thisGet, o, "get this"); + o.s(1); + assert.sameValue(o._p, 1, "set value"); + assert.sameValue(thisSet, o, "set this"); + + thisGet = undefined; + thisSet = undefined; + o.inc(); + assert.sameValue(o._p, 2, "inc value"); + assert.sameValue(thisGet, o, "inc thisGet"); + assert.sameValue(thisSet, o, "inc thisSet"); + + thisGet = undefined; + thisSet = undefined; + assert.sameValue(o.incR(), 2, "incR result"); + assert.sameValue(o._p, 3, "incR value"); + assert.sameValue(thisGet, o, "incR thisGet"); + assert.sameValue(thisSet, o, "incR thisSet"); + + thisGet = undefined; + thisSet = undefined; + o.inc1(); + assert.sameValue(o._p, 4, "inc1 value"); + assert.sameValue(thisGet, o, "inc1 thisGet"); + assert.sameValue(thisSet, o, "inc1 thisSet"); + + thisGet = undefined; + thisSet = undefined; + assert.sameValue(o.inc1R(), 5, "inc1R result"); + assert.sameValue(o._p, 5, "inc1R value"); + assert.sameValue(thisGet, o, "inc1R thisGet"); + assert.sameValue(thisSet, o, "inc1R thisSet"); + + o.pattern(); + assert.sameValue(o._p, 9, "pattern"); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestSuperRefBracketEvalOrder(t *testing.T) { + const SCRIPT = ` + let keyCallCount = 0; + + function key() { + keyCallCount++; + C.prototype.__proto__ = null; + return "k"; + } + + class C { + constructor() { + super[key()]++; + } + } + + assert.throws(TypeError, () => new C()); + assert.sameValue(keyCallCount, 1); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestSuperRefBracketCallee(t *testing.T) { + const SCRIPT = ` + let PROP = "p"; + class P { + get p() { + return function() { + return this; + }; + } + } + + class C extends P { + m() { + return super[PROP](); + } + } + + let o = new C(); + o.m() === o; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestSuperBaseInCtor(t *testing.T) { + const SCRIPT = ` + let result; + class Derived extends Object { + constructor() { + super(); + result = super.constructor === Object; + } + } + new Derived(); + result; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestClassNamedEval(t *testing.T) { + const SCRIPT = ` + const C = class { + } + + C.name === "C"; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestClassNonDerived(t *testing.T) { + const SCRIPT = ` + function initF() { + } + class C { + f = initF() + } + let c = new C(); + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestClassExpr(t *testing.T) { + const SCRIPT = ` + typeof Object.getOwnPropertyDescriptor(class {get f() {}}.prototype, "f").get === "function"; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestClassSuperInHeritage(t *testing.T) { + const SCRIPT = ` + class P { + a() { + return Error; + } + } + + class C extends P { + m() { + class Inner extends super.a() { + } + return new Inner(); + } + } + + new C().m() instanceof Error; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestClassSuperInHeritageExpr(t *testing.T) { + const SCRIPT = ` + class P { + a() { + return Error; + } + } + + class C extends P { + m() { + function f(cls) { + return new cls(); + } + return f(class Inner extends super.a() { + }) + } + } + + new C().m() instanceof Error; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestClassReferToBinding(t *testing.T) { + const SCRIPT = ` + const CC = class C { + static T = 40 + f = C.T + 2 + } + let c = new CC(); + c.f; + ` + + testScript(SCRIPT, intToValue(42), t) +} + +func TestClassReferToBindingInStaticDecl(t *testing.T) { + const SCRIPT = ` + class C { + static T = C.name + } + C.T; + ` + + testScript(SCRIPT, asciiString("C"), t) +} + +func TestClassReferToBindingInStaticEval(t *testing.T) { + const SCRIPT = ` + const CC = class C { + static T = eval("C.name") + } + CC.T; + ` + + testScript(SCRIPT, asciiString("C"), t) +} + +func TestClassReferToBindingFromHeritage(t *testing.T) { + const SCRIPT = ` + assert.throws(ReferenceError, () => { + class C extends C { + } + }); + ` + + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestClassCaptureSuperCallInArrowFunc(t *testing.T) { + const SCRIPT = ` + let f; + class C extends class {} { + constructor() { + f = () => super(); + f(); + } + } + let c = new C(); + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestClassCaptureSuperCallInNestedArrowFunc(t *testing.T) { + const SCRIPT = ` + let f; + class P { + } + class C extends P { + constructor() { + f = () => () => super(); + f()(); + } + } + new C() instanceof P; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestThisInEval(t *testing.T) { + const SCRIPT = ` + assert.sameValue(eval("this"), this, "global"); + + let o = { + f() { + return eval("this"); + } + } + assert.sameValue(o.f(), o, "obj literal"); + ` + + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestStaticAsBindingTarget(t *testing.T) { + const SCRIPT = ` + let [static] = []; + ` + testScript(SCRIPT, _undefined, t) +} + +func TestEvalInStaticFieldInit(t *testing.T) { + const SCRIPT = ` + var C = class { + static f = 'test'; + static g = this.f + '262'; + static h = eval('this.g') + 'test'; + } + C.f === "test" && C.g === "test262" && C.h === "test262test"; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestClassPrivateElemInEval(t *testing.T) { + const SCRIPT = ` + let f1, f2; + + class C extends (f1 = o => eval("o.#a"), Object) { + static #a = 42; + static { + f2 = o => eval("o.#a"); + assert.sameValue(C.#a, 42); + assert.sameValue((() => C.#a)(), 42); + } + } + + assert.throws(SyntaxError, () => f1(C)); + assert.sameValue(f2(C), 42); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestClassPrivateElemInIndirectEval(t *testing.T) { + const SCRIPT = ` + let f1, f2; + + class C extends (f1 = o => (0, eval)("o.#a"), Object) { + static #a = 42; + static { + f2 = o => (0, eval)("o.#a"); + assert.throws(SyntaxError, () => (0, eval)("C.#a")); + } + } + + assert.throws(SyntaxError, () => f1(C)); + assert.throws(SyntaxError, () => f2(C)); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestClassPrivateElemInFunction(t *testing.T) { + const SCRIPT = ` + assert.throws(SyntaxError, () => { + class C { + static #a = 42; + static { + Function("o", "return o.#a"); + } + } + }); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestClassPrivateElementsDecl(t *testing.T) { + const SCRIPT = ` + class C { + #a = 42; + get #b() {} + set #b(_) {} + get c() { + return this.#a; + } + #m() { + return this.#a; + } + static getter(inst) { + return inst.#m(); + } + } + let c = new C(); + c.c + C.getter(c); + ` + testScript(SCRIPT, intToValue(84), t) +} + +func TestPrivateIn(t *testing.T) { + const SCRIPT = ` + class C { + #a = 42; + static check(inst) { + return #a in inst; + } + } + let c = new C(); + C.check(c); + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestDeletePropOfNonObject(t *testing.T) { + const SCRIPT = ` + delete 'Test262'[100] && delete 'Test262'.a && delete 'Test262'['@']; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestKeywordsAsLabels(t *testing.T) { + const SCRIPT = ` + let: for (let i = 0; i < 2; i++) { + if (i === 0) continue let; + break let; + } + + \u006Cet: for (let i = 0; i < 2; i++) { + if (i === 0) continue \u006Cet; + break \u006Cet; + } + + yield: for (let i = 0; i < 2; i++) { + if (i === 0) continue yield; + break yield; + } + + yi\u0065ld: for (let i = 0; i < 2; i++) { + if (i === 0) continue yi\u0065ld; + break yi\u0065ld; + } +` + testScript(SCRIPT, _undefined, t) +} + +func TestThisResolutionWithArg(t *testing.T) { + const SCRIPT = ` + let capture; + function f(arg) { + capture = () => this; // move 'this' to stash + return [this, arg]; + } + const _this = {}; + const arg = {}; + const [_this1, arg1] = f.call(_this, arg); + _this1 === _this && arg1 === arg; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestThisResolutionArgInStash(t *testing.T) { + const SCRIPT = ` + let capture; + function f(arg) { + capture = () => this + arg; // move 'this' and arguments to stash + return [this, arg]; + } + const _this = {}; + const arg = {}; + const [_this1, arg1] = f.call(_this, arg); + _this1 === _this && arg1 === arg; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestThisResolutionWithStackVar(t *testing.T) { + const SCRIPT = ` + let capture; + function f(arg) { + const _ = 1; // a stack variable + capture = () => this + arg; // move 'this' and arguments to stash + return [this, arg]; + } + const _this = {}; + const arg = {}; + const [_this1, arg1] = f.call(_this, arg); + _this1 === _this && arg1 === arg; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestForInLoopContinue(t *testing.T) { + const SCRIPT = ` + var globalSink; + (function() { + const data = [{disabled: true}, {}]; + function dummy() {} + function f1() {} + + function f() { + dummy(); // move dummy to stash (so that f1 is at index 1) + for (const d of data) { + if (d.disabled) continue; + globalSink = () => d; // move d to stash + f1(); + } + } + + f(); + })(); + ` + testScript(SCRIPT, _undefined, t) +} + +func TestForInLoopContinueOuter(t *testing.T) { + const SCRIPT = ` + var globalSink; + (function() { + const data = [{disabled: true}, {}]; + function dummy1() {} + function f1() {} + + function f() { + dummy1(); + let counter = 0; + OUTER: for (let i = 0; i < 1; i++) { + for (const d of data) { + if (d.disabled) continue OUTER; + globalSink = () => d; + } + counter++; + } + f1(); + if (counter !== 0) { + throw new Error(counter); + } + } + + f(); + })(); + ` + testScript(SCRIPT, _undefined, t) +} + +func TestLexicalDeclInSwitch(t *testing.T) { + const SCRIPT = ` + switch(0) { + case 1: + if (false) b = 3; + case 2: + const c = 1; + } + ` + testScript(SCRIPT, _undefined, t) +} + +func TestClassFieldSpecial(t *testing.T) { + const SCRIPT = ` + class C { + get; + set; + async; + static; + } + ` + testScript(SCRIPT, _undefined, t) +} + +func TestClassMethodSpecial(t *testing.T) { + const SCRIPT = ` + class C { + get() {} + set() {} + async() {} + static() {} + } + ` + testScript(SCRIPT, _undefined, t) +} + +func TestClassMethodNumLiteral(t *testing.T) { + const SCRIPT = ` + class C { + 0() { + return true; + } + } + new C()[0](); + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestAsyncFunc(t *testing.T) { + const SCRIPT = ` + async (x = true, y) => {}; + async x => {}; + let passed = false; + async function f() { + return true; + } + async function f1(arg = true) { + passed = await f(); + } + await f1(); + return passed; + ` + testAsyncFunc(SCRIPT, valueTrue, t) +} + +func TestObjectLiteralComputedMethodKeys(t *testing.T) { + _, err := Compile("", ` + ({ + ["__proto__"]() {}, + ["__proto__"]() {} + }) + `, false) + if err != nil { + t.Fatal(err) + } + + _, err = Compile("", ` + ({ + get ["__proto__"]() {}, + get ["__proto__"]() {} + }) + `, false) + if err != nil { + t.Fatal(err) + } +} + +func TestGeneratorFunc(t *testing.T) { + const SCRIPT = ` + let trace = ""; + function defParam() { + trace += "1"; + return "def"; + } + function* g(param = defParam()) { + const THREE = 3; + trace += "2"; + assert.sameValue(Math.floor(yield 1), THREE); + return 42; + } + let iter = g(); + assert.sameValue(trace, "1"); + + let next = iter.next(); + assert.sameValue(next.value, 1); + assert.sameValue(next.done, false); + + next = iter.next(Math.PI); + assert.sameValue(next.value, 42); + assert.sameValue(next.done, true); + + assert.sameValue(trace, "12"); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestGeneratorMethods(t *testing.T) { + const SCRIPT = ` + class C { + *g(param) { + yield 1; + yield 2; + } + } + let c = new C(); + let iter = c.g(); + let res = iter.next(); + assert.sameValue(res.value, 1); + assert.sameValue(res.done, false); + + res = iter.next(); + assert.sameValue(res.value, 2); + assert.sameValue(res.done, false); + + res = iter.next(); + assert.sameValue(res.value, undefined); + assert.sameValue(res.done, true); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestFunctionBodyClassDecl(t *testing.T) { + const SCRIPT = ` + function as(requiredArgument = {}) { + class something { } + }; + ` + _, err := Compile("", SCRIPT, false) + if err != nil { + t.Fatal(err) + } +} + +/* +func TestBabel(t *testing.T) { + src, err := os.ReadFile("babel7.js") + if err != nil { + t.Fatal(err) + } + vm := New() + _, err = vm.RunString(string(src)) + if err != nil { + t.Fatal(err) + } + _, err = vm.RunString(`var result = Babel.transform("", {presets: ["es2015"]});`) + if err != nil { + t.Fatal(err) + } +}*/ + +func BenchmarkCompile(b *testing.B) { + data, err := os.ReadFile("testdata/S15.10.2.12_A1_T1.js") + if err != nil { + b.Fatal(err) + } + + src := string(data) + + for i := 0; i < b.N; i++ { + _, err := Compile("test.js", src, false) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/goja/date.go b/goja/date.go new file mode 100644 index 0000000..ee29a21 --- /dev/null +++ b/goja/date.go @@ -0,0 +1,170 @@ +package goja + +import ( + "math" + "reflect" + "time" +) + +const ( + dateTimeLayout = "Mon Jan 02 2006 15:04:05 GMT-0700 (MST)" + utcDateTimeLayout = "Mon, 02 Jan 2006 15:04:05 GMT" + isoDateTimeLayout = "2006-01-02T15:04:05.000Z" + dateLayout = "Mon Jan 02 2006" + timeLayout = "15:04:05 GMT-0700 (MST)" + datetimeLayout_en_GB = "01/02/2006, 15:04:05" + dateLayout_en_GB = "01/02/2006" + timeLayout_en_GB = "15:04:05" + + maxTime = 8.64e15 + timeUnset = math.MinInt64 +) + +type dateObject struct { + baseObject + msec int64 +} + +type dateLayoutDesc struct { + layout string + dateOnly bool +} + +var ( + dateLayoutsNumeric = []dateLayoutDesc{ + {layout: "2006-01-02T15:04:05Z0700"}, + {layout: "2006-01-02T15:04:05"}, + {layout: "2006-01-02", dateOnly: true}, + {layout: "2006-01-02 15:04:05"}, + + {layout: "2006", dateOnly: true}, + {layout: "2006-01", dateOnly: true}, + + {layout: "2006T15:04"}, + {layout: "2006-01T15:04"}, + {layout: "2006-01-02T15:04"}, + + {layout: "2006T15:04:05"}, + {layout: "2006-01T15:04:05"}, + + {layout: "2006T15:04Z0700"}, + {layout: "2006-01T15:04Z0700"}, + {layout: "2006-01-02T15:04Z0700"}, + + {layout: "2006T15:04:05Z0700"}, + {layout: "2006-01T15:04:05Z0700"}, + } + + dateLayoutsAlpha = []dateLayoutDesc{ + {layout: time.RFC1123}, + {layout: time.RFC1123Z}, + {layout: dateTimeLayout}, + {layout: time.UnixDate}, + {layout: time.ANSIC}, + {layout: time.RubyDate}, + {layout: "Mon, _2 Jan 2006 15:04:05 GMT-0700 (MST)"}, + {layout: "Mon, _2 Jan 2006 15:04:05 -0700 (MST)"}, + {layout: "Jan _2, 2006", dateOnly: true}, + } +) + +func dateParse(date string) (time.Time, bool) { + var t time.Time + var err error + var layouts []dateLayoutDesc + if len(date) > 0 { + first := date[0] + if first <= '9' && (first >= '0' || first == '-' || first == '+') { + layouts = dateLayoutsNumeric + } else { + layouts = dateLayoutsAlpha + } + } else { + return time.Time{}, false + } + for _, desc := range layouts { + var defLoc *time.Location + if desc.dateOnly { + defLoc = time.UTC + } else { + defLoc = time.Local + } + t, err = parseDate(desc.layout, date, defLoc) + if err == nil { + break + } + } + if err != nil { + return time.Time{}, false + } + unix := timeToMsec(t) + return t, unix >= -maxTime && unix <= maxTime +} + +func (r *Runtime) newDateObject(t time.Time, isSet bool, proto *Object) *Object { + v := &Object{runtime: r} + d := &dateObject{} + v.self = d + d.val = v + d.class = classDate + d.prototype = proto + d.extensible = true + d.init() + if isSet { + d.msec = timeToMsec(t) + } else { + d.msec = timeUnset + } + return v +} + +func dateFormat(t time.Time) string { + return t.Local().Format(dateTimeLayout) +} + +func timeFromMsec(msec int64) time.Time { + sec := msec / 1000 + nsec := (msec % 1000) * 1e6 + return time.Unix(sec, nsec) +} + +func timeToMsec(t time.Time) int64 { + return t.Unix()*1000 + int64(t.Nanosecond())/1e6 +} + +func (d *dateObject) exportType() reflect.Type { + return typeTime +} + +func (d *dateObject) export(*objectExportCtx) interface{} { + if d.isSet() { + return d.time() + } + return nil +} + +func (d *dateObject) setTimeMs(ms int64) Value { + if ms >= 0 && ms <= maxTime || ms < 0 && ms >= -maxTime { + d.msec = ms + return intToValue(ms) + } + + d.unset() + return _NaN +} + +func (d *dateObject) isSet() bool { + return d.msec != timeUnset +} + +func (d *dateObject) unset() { + d.msec = timeUnset +} + +func (d *dateObject) time() time.Time { + return timeFromMsec(d.msec) +} + +func (d *dateObject) timeUTC() time.Time { + return timeFromMsec(d.msec).In(time.UTC) +} diff --git a/goja/date_parser.go b/goja/date_parser.go new file mode 100644 index 0000000..762888c --- /dev/null +++ b/goja/date_parser.go @@ -0,0 +1,869 @@ +package goja + +// This is a slightly modified version of the standard Go parser to make it more compatible with ECMAScript 5.1 +// Changes: +// - 6-digit extended years are supported in place of long year (2006) in the form of +123456 +// - Timezone formats tolerate colons, e.g. -0700 will parse -07:00 +// - Short week day will also parse long week day +// - Short month ("Jan") will also parse long month ("January") +// - Long day ("02") will also parse short day ("2"). +// - Timezone in brackets, "(MST)", will match any string in brackets (e.g. "(GMT Standard Time)") +// - If offset is not set and timezone name is unknown, an error is returned +// - If offset and timezone name are both set the offset takes precedence and the resulting Location will be FixedZone("", offset) + +// Original copyright message: + +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +import ( + "errors" + "time" +) + +const ( + _ = iota + stdLongMonth = iota + stdNeedDate // "January" + stdMonth // "Jan" + stdNumMonth // "1" + stdZeroMonth // "01" + stdLongWeekDay // "Monday" + stdWeekDay // "Mon" + stdDay // "2" + stdUnderDay // "_2" + stdZeroDay // "02" + stdHour = iota + stdNeedClock // "15" + stdHour12 // "3" + stdZeroHour12 // "03" + stdMinute // "4" + stdZeroMinute // "04" + stdSecond // "5" + stdZeroSecond // "05" + stdLongYear = iota + stdNeedDate // "2006" + stdYear // "06" + stdPM = iota + stdNeedClock // "PM" + stdpm // "pm" + stdTZ = iota // "MST" + stdBracketTZ // "(MST)" + stdISO8601TZ // "Z0700" // prints Z for UTC + stdISO8601SecondsTZ // "Z070000" + stdISO8601ShortTZ // "Z07" + stdISO8601ColonTZ // "Z07:00" // prints Z for UTC + stdISO8601ColonSecondsTZ // "Z07:00:00" + stdNumTZ // "-0700" // always numeric + stdNumSecondsTz // "-070000" + stdNumShortTZ // "-07" // always numeric + stdNumColonTZ // "-07:00" // always numeric + stdNumColonSecondsTZ // "-07:00:00" + stdFracSecond0 // ".0", ".00", ... , trailing zeros included + stdFracSecond9 // ".9", ".99", ..., trailing zeros omitted + + stdNeedDate = 1 << 8 // need month, day, year + stdNeedClock = 2 << 8 // need hour, minute, second + stdArgShift = 16 // extra argument in high bits, above low stdArgShift + stdMask = 1<= 69 { // Unix time starts Dec 31 1969 in some time zones + year += 1900 + } else { + year += 2000 + } + case stdLongYear: + if len(value) >= 7 && (value[0] == '-' || value[0] == '+') { // extended year + neg := value[0] == '-' + p, value = value[1:7], value[7:] + year, err = atoi(p) + if neg { + if year == 0 { + err = errBad + break + } + year = -year + } + } else { + if len(value) < 4 || !isDigit(value, 0) { + err = errBad + break + } + p, value = value[0:4], value[4:] + year, err = atoi(p) + } + + case stdMonth: + month, value, err = lookup(longMonthNames, value) + if err != nil { + month, value, err = lookup(shortMonthNames, value) + } + month++ + case stdLongMonth: + month, value, err = lookup(longMonthNames, value) + month++ + case stdNumMonth, stdZeroMonth: + month, value, err = getnum(value, std == stdZeroMonth) + if month <= 0 || 12 < month { + rangeErrString = "month" + } + case stdWeekDay: + // Ignore weekday except for error checking. + _, value, err = lookup(longDayNames, value) + if err != nil { + _, value, err = lookup(shortDayNames, value) + } + case stdLongWeekDay: + _, value, err = lookup(longDayNames, value) + case stdDay, stdUnderDay, stdZeroDay: + if std == stdUnderDay && len(value) > 0 && value[0] == ' ' { + value = value[1:] + } + day, value, err = getnum(value, false) + if day < 0 { + // Note that we allow any one- or two-digit day here. + rangeErrString = "day" + } + case stdHour: + hour, value, err = getnum(value, false) + if hour < 0 || 24 <= hour { + rangeErrString = "hour" + } + case stdHour12, stdZeroHour12: + hour, value, err = getnum(value, std == stdZeroHour12) + if hour < 0 || 12 < hour { + rangeErrString = "hour" + } + case stdMinute, stdZeroMinute: + min, value, err = getnum(value, std == stdZeroMinute) + if min < 0 || 60 <= min { + rangeErrString = "minute" + } + case stdSecond, stdZeroSecond: + sec, value, err = getnum(value, std == stdZeroSecond) + if sec < 0 || 60 <= sec { + rangeErrString = "second" + break + } + // Special case: do we have a fractional second but no + // fractional second in the format? + if len(value) >= 2 && value[0] == '.' && isDigit(value, 1) { + _, std, _ = nextStdChunk(layout) + std &= stdMask + if std == stdFracSecond0 || std == stdFracSecond9 { + // Fractional second in the layout; proceed normally + break + } + // No fractional second in the layout but we have one in the input. + n := 2 + for ; n < len(value) && isDigit(value, n); n++ { + } + nsec, rangeErrString, err = parseNanoseconds(value, n) + value = value[n:] + } + case stdPM: + if len(value) < 2 { + err = errBad + break + } + p, value = value[0:2], value[2:] + switch p { + case "PM": + pmSet = true + case "AM": + amSet = true + default: + err = errBad + } + case stdpm: + if len(value) < 2 { + err = errBad + break + } + p, value = value[0:2], value[2:] + switch p { + case "pm": + pmSet = true + case "am": + amSet = true + default: + err = errBad + } + case stdISO8601TZ, stdISO8601ColonTZ, stdISO8601SecondsTZ, stdISO8601ShortTZ, stdISO8601ColonSecondsTZ, stdNumTZ, stdNumShortTZ, stdNumColonTZ, stdNumSecondsTz, stdNumColonSecondsTZ: + if (std == stdISO8601TZ || std == stdISO8601ShortTZ || std == stdISO8601ColonTZ || + std == stdISO8601SecondsTZ || std == stdISO8601ColonSecondsTZ) && len(value) >= 1 && value[0] == 'Z' { + + value = value[1:] + z = time.UTC + break + } + var sign, hour, min, seconds string + if std == stdISO8601ColonTZ || std == stdNumColonTZ || std == stdNumTZ || std == stdISO8601TZ { + if len(value) < 4 { + err = errBad + break + } + if value[3] != ':' { + if std == stdNumColonTZ || std == stdISO8601ColonTZ || len(value) < 5 { + err = errBad + break + } + sign, hour, min, seconds, value = value[0:1], value[1:3], value[3:5], "00", value[5:] + } else { + if len(value) < 6 { + err = errBad + break + } + sign, hour, min, seconds, value = value[0:1], value[1:3], value[4:6], "00", value[6:] + } + } else if std == stdNumShortTZ || std == stdISO8601ShortTZ { + if len(value) < 3 { + err = errBad + break + } + sign, hour, min, seconds, value = value[0:1], value[1:3], "00", "00", value[3:] + } else if std == stdISO8601ColonSecondsTZ || std == stdNumColonSecondsTZ || std == stdISO8601SecondsTZ || std == stdNumSecondsTz { + if len(value) < 7 { + err = errBad + break + } + if value[3] != ':' || value[6] != ':' { + if std == stdISO8601ColonSecondsTZ || std == stdNumColonSecondsTZ || len(value) < 7 { + err = errBad + break + } + sign, hour, min, seconds, value = value[0:1], value[1:3], value[3:5], value[5:7], value[7:] + } else { + if len(value) < 9 { + err = errBad + break + } + sign, hour, min, seconds, value = value[0:1], value[1:3], value[4:6], value[7:9], value[9:] + } + } + var hr, mm, ss int + hr, err = atoi(hour) + if err == nil { + mm, err = atoi(min) + } + if err == nil { + ss, err = atoi(seconds) + } + zoneOffset = (hr*60+mm)*60 + ss // offset is in seconds + switch sign[0] { + case '+': + case '-': + zoneOffset = -zoneOffset + default: + err = errBad + } + case stdTZ: + // Does it look like a time zone? + if len(value) >= 3 && value[0:3] == "UTC" { + z = time.UTC + value = value[3:] + break + } + n, ok := parseTimeZone(value) + if !ok { + err = errBad + break + } + zoneName, value = value[:n], value[n:] + case stdBracketTZ: + if len(value) < 3 || value[0] != '(' { + err = errBad + break + } + i := 1 + for ; ; i++ { + if i >= len(value) { + err = errBad + break + } + if value[i] == ')' { + zoneName, value = value[1:i], value[i+1:] + break + } + } + + case stdFracSecond0: + // stdFracSecond0 requires the exact number of digits as specified in + // the layout. + ndigit := 1 + (std >> stdArgShift) + if len(value) < ndigit { + err = errBad + break + } + nsec, rangeErrString, err = parseNanoseconds(value, ndigit) + value = value[ndigit:] + + case stdFracSecond9: + if len(value) < 2 || value[0] != '.' || value[1] < '0' || '9' < value[1] { + // Fractional second omitted. + break + } + // Take any number of digits, even more than asked for, + // because it is what the stdSecond case would do. + i := 0 + for i < 9 && i+1 < len(value) && '0' <= value[i+1] && value[i+1] <= '9' { + i++ + } + nsec, rangeErrString, err = parseNanoseconds(value, 1+i) + value = value[1+i:] + } + if rangeErrString != "" { + return time.Time{}, &time.ParseError{Layout: alayout, Value: avalue, LayoutElem: stdstr, ValueElem: value, Message: ": " + rangeErrString + " out of range"} + } + if err != nil { + return time.Time{}, &time.ParseError{Layout: alayout, Value: avalue, LayoutElem: stdstr, ValueElem: value} + } + } + if pmSet && hour < 12 { + hour += 12 + } else if amSet && hour == 12 { + hour = 0 + } + + // Validate the day of the month. + if day < 1 || day > daysIn(time.Month(month), year) { + return time.Time{}, &time.ParseError{Layout: alayout, Value: avalue, ValueElem: value, Message: ": day out of range"} + } + + if z == nil { + if zoneOffset == -1 { + if zoneName != "" { + if z1, err := time.LoadLocation(zoneName); err == nil { + z = z1 + } else { + return time.Time{}, &time.ParseError{Layout: alayout, Value: avalue, ValueElem: value, Message: ": unknown timezone"} + } + } else { + z = defaultLocation + } + } else if zoneOffset == 0 { + z = time.UTC + } else { + z = time.FixedZone("", zoneOffset) + } + } + + return time.Date(year, time.Month(month), day, hour, min, sec, nsec, z), nil +} + +var errLeadingInt = errors.New("time: bad [0-9]*") // never printed + +func signedLeadingInt(s string) (x int64, rem string, err error) { + neg := false + if s != "" && (s[0] == '-' || s[0] == '+') { + neg = s[0] == '-' + s = s[1:] + } + x, rem, err = leadingInt(s) + if err != nil { + return + } + + if neg { + x = -x + } + return +} + +// leadingInt consumes the leading [0-9]* from s. +func leadingInt(s string) (x int64, rem string, err error) { + i := 0 + for ; i < len(s); i++ { + c := s[i] + if c < '0' || c > '9' { + break + } + if x > (1<<63-1)/10 { + // overflow + return 0, "", errLeadingInt + } + x = x*10 + int64(c) - '0' + if x < 0 { + // overflow + return 0, "", errLeadingInt + } + } + return x, s[i:], nil +} + +// nextStdChunk finds the first occurrence of a std string in +// layout and returns the text before, the std string, and the text after. +func nextStdChunk(layout string) (prefix string, std int, suffix string) { + for i := 0; i < len(layout); i++ { + switch c := int(layout[i]); c { + case 'J': // January, Jan + if len(layout) >= i+3 && layout[i:i+3] == "Jan" { + if len(layout) >= i+7 && layout[i:i+7] == "January" { + return layout[0:i], stdLongMonth, layout[i+7:] + } + if !startsWithLowerCase(layout[i+3:]) { + return layout[0:i], stdMonth, layout[i+3:] + } + } + + case 'M': // Monday, Mon, MST + if len(layout) >= i+3 { + if layout[i:i+3] == "Mon" { + if len(layout) >= i+6 && layout[i:i+6] == "Monday" { + return layout[0:i], stdLongWeekDay, layout[i+6:] + } + if !startsWithLowerCase(layout[i+3:]) { + return layout[0:i], stdWeekDay, layout[i+3:] + } + } + if layout[i:i+3] == "MST" { + return layout[0:i], stdTZ, layout[i+3:] + } + } + + case '0': // 01, 02, 03, 04, 05, 06 + if len(layout) >= i+2 && '1' <= layout[i+1] && layout[i+1] <= '6' { + return layout[0:i], std0x[layout[i+1]-'1'], layout[i+2:] + } + + case '1': // 15, 1 + if len(layout) >= i+2 && layout[i+1] == '5' { + return layout[0:i], stdHour, layout[i+2:] + } + return layout[0:i], stdNumMonth, layout[i+1:] + + case '2': // 2006, 2 + if len(layout) >= i+4 && layout[i:i+4] == "2006" { + return layout[0:i], stdLongYear, layout[i+4:] + } + return layout[0:i], stdDay, layout[i+1:] + + case '_': // _2, _2006 + if len(layout) >= i+2 && layout[i+1] == '2' { + //_2006 is really a literal _, followed by stdLongYear + if len(layout) >= i+5 && layout[i+1:i+5] == "2006" { + return layout[0 : i+1], stdLongYear, layout[i+5:] + } + return layout[0:i], stdUnderDay, layout[i+2:] + } + + case '3': + return layout[0:i], stdHour12, layout[i+1:] + + case '4': + return layout[0:i], stdMinute, layout[i+1:] + + case '5': + return layout[0:i], stdSecond, layout[i+1:] + + case 'P': // PM + if len(layout) >= i+2 && layout[i+1] == 'M' { + return layout[0:i], stdPM, layout[i+2:] + } + + case 'p': // pm + if len(layout) >= i+2 && layout[i+1] == 'm' { + return layout[0:i], stdpm, layout[i+2:] + } + + case '-': // -070000, -07:00:00, -0700, -07:00, -07 + if len(layout) >= i+7 && layout[i:i+7] == "-070000" { + return layout[0:i], stdNumSecondsTz, layout[i+7:] + } + if len(layout) >= i+9 && layout[i:i+9] == "-07:00:00" { + return layout[0:i], stdNumColonSecondsTZ, layout[i+9:] + } + if len(layout) >= i+5 && layout[i:i+5] == "-0700" { + return layout[0:i], stdNumTZ, layout[i+5:] + } + if len(layout) >= i+6 && layout[i:i+6] == "-07:00" { + return layout[0:i], stdNumColonTZ, layout[i+6:] + } + if len(layout) >= i+3 && layout[i:i+3] == "-07" { + return layout[0:i], stdNumShortTZ, layout[i+3:] + } + + case 'Z': // Z070000, Z07:00:00, Z0700, Z07:00, + if len(layout) >= i+7 && layout[i:i+7] == "Z070000" { + return layout[0:i], stdISO8601SecondsTZ, layout[i+7:] + } + if len(layout) >= i+9 && layout[i:i+9] == "Z07:00:00" { + return layout[0:i], stdISO8601ColonSecondsTZ, layout[i+9:] + } + if len(layout) >= i+5 && layout[i:i+5] == "Z0700" { + return layout[0:i], stdISO8601TZ, layout[i+5:] + } + if len(layout) >= i+6 && layout[i:i+6] == "Z07:00" { + return layout[0:i], stdISO8601ColonTZ, layout[i+6:] + } + if len(layout) >= i+3 && layout[i:i+3] == "Z07" { + return layout[0:i], stdISO8601ShortTZ, layout[i+3:] + } + + case '.': // .000 or .999 - repeated digits for fractional seconds. + if i+1 < len(layout) && (layout[i+1] == '0' || layout[i+1] == '9') { + ch := layout[i+1] + j := i + 1 + for j < len(layout) && layout[j] == ch { + j++ + } + // String of digits must end here - only fractional second is all digits. + if !isDigit(layout, j) { + std := stdFracSecond0 + if layout[i+1] == '9' { + std = stdFracSecond9 + } + std |= (j - (i + 1)) << stdArgShift + return layout[0:i], std, layout[j:] + } + } + case '(': + if len(layout) >= i+5 && layout[i:i+5] == "(MST)" { + return layout[0:i], stdBracketTZ, layout[i+5:] + } + } + } + return layout, 0, "" +} + +var longDayNames = []string{ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", +} + +var shortDayNames = []string{ + "Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", +} + +var shortMonthNames = []string{ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", +} + +var longMonthNames = []string{ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +} + +// isDigit reports whether s[i] is in range and is a decimal digit. +func isDigit(s string, i int) bool { + if len(s) <= i { + return false + } + c := s[i] + return '0' <= c && c <= '9' +} + +// getnum parses s[0:1] or s[0:2] (fixed forces the latter) +// as a decimal integer and returns the integer and the +// remainder of the string. +func getnum(s string, fixed bool) (int, string, error) { + if !isDigit(s, 0) { + return 0, s, errBad + } + if !isDigit(s, 1) { + if fixed { + return 0, s, errBad + } + return int(s[0] - '0'), s[1:], nil + } + return int(s[0]-'0')*10 + int(s[1]-'0'), s[2:], nil +} + +func cutspace(s string) string { + for len(s) > 0 && s[0] == ' ' { + s = s[1:] + } + return s +} + +// skip removes the given prefix from value, +// treating runs of space characters as equivalent. +func skip(value, prefix string) (string, error) { + for len(prefix) > 0 { + if prefix[0] == ' ' { + if len(value) > 0 && value[0] != ' ' { + return value, errBad + } + prefix = cutspace(prefix) + value = cutspace(value) + continue + } + if len(value) == 0 || value[0] != prefix[0] { + return value, errBad + } + prefix = prefix[1:] + value = value[1:] + } + return value, nil +} + +// Never printed, just needs to be non-nil for return by atoi. +var atoiError = errors.New("time: invalid number") + +// Duplicates functionality in strconv, but avoids dependency. +func atoi(s string) (x int, err error) { + q, rem, err := signedLeadingInt(s) + x = int(q) + if err != nil || rem != "" { + return 0, atoiError + } + return x, nil +} + +// match reports whether s1 and s2 match ignoring case. +// It is assumed s1 and s2 are the same length. +func match(s1, s2 string) bool { + for i := 0; i < len(s1); i++ { + c1 := s1[i] + c2 := s2[i] + if c1 != c2 { + // Switch to lower-case; 'a'-'A' is known to be a single bit. + c1 |= 'a' - 'A' + c2 |= 'a' - 'A' + if c1 != c2 || c1 < 'a' || c1 > 'z' { + return false + } + } + } + return true +} + +func lookup(tab []string, val string) (int, string, error) { + for i, v := range tab { + if len(val) >= len(v) && match(val[0:len(v)], v) { + return i, val[len(v):], nil + } + } + return -1, val, errBad +} + +// daysBefore[m] counts the number of days in a non-leap year +// before month m begins. There is an entry for m=12, counting +// the number of days before January of next year (365). +var daysBefore = [...]int32{ + 0, + 31, + 31 + 28, + 31 + 28 + 31, + 31 + 28 + 31 + 30, + 31 + 28 + 31 + 30 + 31, + 31 + 28 + 31 + 30 + 31 + 30, + 31 + 28 + 31 + 30 + 31 + 30 + 31, + 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31, + 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30, + 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31, + 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30, + 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30 + 31, +} + +func isLeap(year int) bool { + return year%4 == 0 && (year%100 != 0 || year%400 == 0) +} + +func daysIn(m time.Month, year int) int { + if m == time.February && isLeap(year) { + return 29 + } + return int(daysBefore[m] - daysBefore[m-1]) +} + +// parseTimeZone parses a time zone string and returns its length. Time zones +// are human-generated and unpredictable. We can't do precise error checking. +// On the other hand, for a correct parse there must be a time zone at the +// beginning of the string, so it's almost always true that there's one +// there. We look at the beginning of the string for a run of upper-case letters. +// If there are more than 5, it's an error. +// If there are 4 or 5 and the last is a T, it's a time zone. +// If there are 3, it's a time zone. +// Otherwise, other than special cases, it's not a time zone. +// GMT is special because it can have an hour offset. +func parseTimeZone(value string) (length int, ok bool) { + if len(value) < 3 { + return 0, false + } + // Special case 1: ChST and MeST are the only zones with a lower-case letter. + if len(value) >= 4 && (value[:4] == "ChST" || value[:4] == "MeST") { + return 4, true + } + // Special case 2: GMT may have an hour offset; treat it specially. + if value[:3] == "GMT" { + length = parseGMT(value) + return length, true + } + // Special Case 3: Some time zones are not named, but have +/-00 format + if value[0] == '+' || value[0] == '-' { + length = parseSignedOffset(value) + return length, true + } + // How many upper-case letters are there? Need at least three, at most five. + var nUpper int + for nUpper = 0; nUpper < 6; nUpper++ { + if nUpper >= len(value) { + break + } + if c := value[nUpper]; c < 'A' || 'Z' < c { + break + } + } + switch nUpper { + case 0, 1, 2, 6: + return 0, false + case 5: // Must end in T to match. + if value[4] == 'T' { + return 5, true + } + case 4: + // Must end in T, except one special case. + if value[3] == 'T' || value[:4] == "WITA" { + return 4, true + } + case 3: + return 3, true + } + return 0, false +} + +// parseGMT parses a GMT time zone. The input string is known to start "GMT". +// The function checks whether that is followed by a sign and a number in the +// range -14 through 12 excluding zero. +func parseGMT(value string) int { + value = value[3:] + if len(value) == 0 { + return 3 + } + + return 3 + parseSignedOffset(value) +} + +// parseSignedOffset parses a signed timezone offset (e.g. "+03" or "-04"). +// The function checks for a signed number in the range -14 through +12 excluding zero. +// Returns length of the found offset string or 0 otherwise +func parseSignedOffset(value string) int { + sign := value[0] + if sign != '-' && sign != '+' { + return 0 + } + x, rem, err := leadingInt(value[1:]) + if err != nil { + return 0 + } + if sign == '-' { + x = -x + } + if x == 0 || x < -14 || 12 < x { + return 0 + } + return len(value) - len(rem) +} + +func parseNanoseconds(value string, nbytes int) (ns int, rangeErrString string, err error) { + if value[0] != '.' { + err = errBad + return + } + if ns, err = atoi(value[1:nbytes]); err != nil { + return + } + if ns < 0 || 1e9 <= ns { + rangeErrString = "fractional second" + return + } + // We need nanoseconds, which means scaling by the number + // of missing digits in the format, maximum length 10. If it's + // longer than 10, we won't scale. + scaleDigits := 10 - nbytes + for i := 0; i < scaleDigits; i++ { + ns *= 10 + } + return +} + +// std0x records the std values for "01", "02", ..., "06". +var std0x = [...]int{stdZeroMonth, stdZeroDay, stdZeroHour12, stdZeroMinute, stdZeroSecond, stdYear} + +// startsWithLowerCase reports whether the string has a lower-case letter at the beginning. +// Its purpose is to prevent matching strings like "Month" when looking for "Mon". +func startsWithLowerCase(str string) bool { + if len(str) == 0 { + return false + } + c := str[0] + return 'a' <= c && c <= 'z' +} diff --git a/goja/date_parser_test.go b/goja/date_parser_test.go new file mode 100644 index 0000000..c9a5446 --- /dev/null +++ b/goja/date_parser_test.go @@ -0,0 +1,31 @@ +package goja + +import ( + "testing" + "time" +) + +func TestParseDate(t *testing.T) { + + tst := func(layout, value string, expectedTs int64) func(t *testing.T) { + return func(t *testing.T) { + t.Parallel() + tm, err := parseDate(layout, value, time.UTC) + if err != nil { + t.Fatal(err) + } + if tm.Unix() != expectedTs { + t.Fatal(tm) + } + } + } + + t.Run("1", tst("2006-01-02T15:04:05.000Z070000", "2006-01-02T15:04:05.000+07:00:00", 1136189045)) + t.Run("2", tst("2006-01-02T15:04:05.000Z07:00:00", "2006-01-02T15:04:05.000+07:00:00", 1136189045)) + t.Run("3", tst("2006-01-02T15:04:05.000Z07:00", "2006-01-02T15:04:05.000+07:00", 1136189045)) + t.Run("4", tst("2006-01-02T15:04:05.000Z070000", "2006-01-02T15:04:05.000+070000", 1136189045)) + t.Run("5", tst("2006-01-02T15:04:05.000Z070000", "2006-01-02T15:04:05.000Z", 1136214245)) + t.Run("6", tst("2006-01-02T15:04:05.000Z0700", "2006-01-02T15:04:05.000Z", 1136214245)) + t.Run("7", tst("2006-01-02T15:04:05.000Z07", "2006-01-02T15:04:05.000Z", 1136214245)) + +} diff --git a/goja/date_test.go b/goja/date_test.go new file mode 100644 index 0000000..fdaa0e7 --- /dev/null +++ b/goja/date_test.go @@ -0,0 +1,309 @@ +package goja + +import ( + "testing" + "time" +) + +func TestDateUTC(t *testing.T) { + const SCRIPT = ` + assert.sameValue(Date.UTC(1970, 0), 0, '1970, 0'); + assert.sameValue(Date.UTC(2016, 0), 1451606400000, '2016, 0'); + assert.sameValue(Date.UTC(2016, 6), 1467331200000, '2016, 6'); + + assert.sameValue(Date.UTC(2016, 6, 1), 1467331200000, '2016, 6, 1'); + assert.sameValue(Date.UTC(2016, 6, 5), 1467676800000, '2016, 6, 5'); + + assert.sameValue(Date.UTC(2016, 6, 5, 0), 1467676800000, '2016, 6, 5, 0'); + assert.sameValue(Date.UTC(2016, 6, 5, 15), 1467730800000, '2016, 6, 5, 15'); + + assert.sameValue( + Date.UTC(2016, 6, 5, 15, 0), 1467730800000, '2016, 6, 5, 15, 0' + ); + assert.sameValue( + Date.UTC(2016, 6, 5, 15, 34), 1467732840000, '2016, 6, 5, 15, 34' + ); + + assert.sameValue( + Date.UTC(2016, 6, 5, 15, 34, 0), 1467732840000, '2016, 6, 5, 15, 34, 0' + ); + assert.sameValue( + Date.UTC(2016, 6, 5, 15, 34, 45), 1467732885000, '2016, 6, 5, 15, 34, 45' + ); + + ` + + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestNewDate(t *testing.T) { + const SCRIPT = ` + var d1 = new Date("2016-09-01T12:34:56Z"); + d1.getUTCHours() === 12; + + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestNewDate0(t *testing.T) { + const SCRIPT = ` + (new Date(0)).toUTCString(); + + ` + testScript(SCRIPT, asciiString("Thu, 01 Jan 1970 00:00:00 GMT"), t) +} + +func TestSetHour(t *testing.T) { + l := time.Local + defer func() { + time.Local = l + }() + var err error + time.Local, err = time.LoadLocation("America/New_York") + if err != nil { + t.Fatal(err) + } + + const SCRIPT = ` + var d = new Date(2016, 8, 1, 12, 23, 45) + assert.sameValue(d.getHours(), 12); + assert.sameValue(d.getUTCHours(), 16); + + d.setHours(13); + assert.sameValue(d.getHours(), 13); + assert.sameValue(d.getMinutes(), 23); + assert.sameValue(d.getSeconds(), 45); + + d.setUTCHours(13); + assert.sameValue(d.getHours(), 9); + assert.sameValue(d.getMinutes(), 23); + assert.sameValue(d.getSeconds(), 45); + + ` + testScriptWithTestLib(SCRIPT, _undefined, t) + +} + +func TestSetMinute(t *testing.T) { + l := time.Local + defer func() { + time.Local = l + }() + time.Local = time.FixedZone("Asia/Delhi", 5*60*60+30*60) + + const SCRIPT = ` + var d = new Date(2016, 8, 1, 12, 23, 45) + assert.sameValue(d.getHours(), 12); + assert.sameValue(d.getUTCHours(), 6); + assert.sameValue(d.getMinutes(), 23); + assert.sameValue(d.getUTCMinutes(), 53); + + d.setMinutes(55); + assert.sameValue(d.getMinutes(), 55); + assert.sameValue(d.getSeconds(), 45); + + d.setUTCMinutes(52); + assert.sameValue(d.getMinutes(), 22); + assert.sameValue(d.getHours(), 13); + + ` + testScriptWithTestLib(SCRIPT, _undefined, t) + +} + +func TestTimezoneOffset(t *testing.T) { + const SCRIPT = ` + var d = new Date(0); + d.getTimezoneOffset(); + ` + + l := time.Local + defer func() { + time.Local = l + }() + var err error + time.Local, err = time.LoadLocation("Europe/London") + if err != nil { + t.Fatal(err) + } + + testScript(SCRIPT, intToValue(-60), t) +} + +func TestDateValueOf(t *testing.T) { + const SCRIPT = ` + var d9 = new Date(1.23e15); + d9.valueOf(); + ` + + testScript(SCRIPT, intToValue(1.23e15), t) +} + +func TestDateSetters(t *testing.T) { + const SCRIPT = ` + assert.sameValue((new Date(0)).setMilliseconds(2345), 2345, "setMilliseconds(2345)"); + assert.sameValue(new Date(1000).setMilliseconds(23450000000000), 23450000001000, "setMilliseconds(23450000000000)"); + assert.sameValue((new Date(0)).setUTCMilliseconds(2345), 2345, "setUTCMilliseconds()"); + assert.sameValue((new Date(0)).setSeconds(12), 12000, "setSeconds()"); + assert.sameValue((new Date(0)).setUTCSeconds(12), 12000, "setUTCSeconds()"); + assert.sameValue((new Date(0)).setMinutes(12), 12 * 60 * 1000, "setMinutes()"); + assert.sameValue((new Date(0)).setUTCMinutes(12), 12 * 60 * 1000, "setUTCMinutes()"); + assert.sameValue((new Date("2016-06-01")).setHours(1), 1464739200000, "setHours()"); + assert.sameValue((new Date("2016-06-01")).setUTCHours(1), 1464742800000, "setUTCHours()"); + assert.sameValue((new Date(0)).setDate(2), 86400000, "setDate()"); + assert.sameValue((new Date(0)).setUTCDate(2), 86400000, "setUTCDate()"); + assert.sameValue((new Date(0)).setMonth(2), 5097600000, "setMonth()"); + assert.sameValue((new Date(0)).setUTCMonth(2), 5097600000, "setUTCMonth()"); + assert.sameValue((new Date(0)).setFullYear(1971), 31536000000, "setFullYear()"); + assert.sameValue((new Date(0)).setFullYear(1971, 2, 3), 36806400000, "setFullYear(Y,M,D)"); + assert.sameValue((new Date(0)).setUTCFullYear(1971), 31536000000, "setUTCFullYear()"); + assert.sameValue((new Date(0)).setUTCFullYear(1971, 2, 3), 36806400000, "setUTCFullYear(Y,M,D)"); + + var d = new Date(); + d.setTime(1151877845000); + assert.sameValue(d.getHours(), 23, "d.getHours()"); + ` + + l := time.Local + defer func() { + time.Local = l + }() + var err error + time.Local, err = time.LoadLocation("Europe/London") + if err != nil { + t.Fatal(err) + } + + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestDateParse(t *testing.T) { + const SCRIPT = ` + var zero = new Date(0); + + assert.sameValue(zero.valueOf(), Date.parse(zero.toString()), + "Date.parse(zeroDate.toString())"); + assert.sameValue(zero.valueOf(), Date.parse(zero.toUTCString()), + "Date.parse(zeroDate.toUTCString())"); + assert.sameValue(zero.valueOf(), Date.parse(zero.toISOString()), + "Date.parse(zeroDate.toISOString())"); + + function testParse(str, expected) { + assert.sameValue(Date.parse(str), expected, str); + } + + testParse("Mon, 02 Jan 2006 15:04:05 MST", 1136239445000); + testParse("Tue, 22 Jun 2021 13:54:40 GMT", 1624370080000); + testParse("Tuesday, 22 Jun 2021 13:54:40 GMT", 1624370080000); + testParse("Mon, 02 Jan 2006 15:04:05 GMT-07:00 (MST)", 1136239445000); + testParse("Mon, 02 Jan 2006 15:04:05 -07:00 (MST)", 1136239445000); + testParse("Monday, 02 Jan 2006 15:04:05 -0700 (MST)", 1136239445000); + testParse("Mon Jan 02 2006 15:04:05 GMT-0700 (GMT Standard Time)", 1136239445000); + testParse("Mon Jan 2 15:04:05 MST 2006", 1136239445000); + testParse("Mon Jan 02 15:04:05 MST 2006", 1136239445000); + testParse("Mon Jan 02 15:04:05 -0700 2006", 1136239445000); + + testParse("December 04, 1986", 534038400000); + testParse("Dec 04, 1986", 534038400000); + testParse("Dec 4, 1986", 534038400000); + + testParse("2006-01-02T15:04:05.000Z", 1136214245000); + testParse("2006-06-02T15:04:05.000", 1149275045000); + testParse("2006-01-02T15:04:05", 1136232245000); + testParse("2006-01-02", 1136160000000); + testParse("2006T15:04-0700", 1136153040000); + testParse("2006T15:04Z", 1136127840000); + testParse("2019-01-01T12:00:00.52Z", 1546344000520); + + var d = new Date("Mon, 02 Jan 2006 15:04:05 MST"); + + assert.sameValue(d.getUTCHours(), 22, + "new Date(\"Mon, 02 Jan 2006 15:04:05 MST\").getUTCHours()"); + + assert.sameValue(d.getHours(), 17, + "new Date(\"Mon, 02 Jan 2006 15:04:05 MST\").getHours()"); + + assert.sameValue(Date.parse("Mon, 02 Jan 2006 15:04:05 zzz"), NaN, + "Date.parse(\"Mon, 02 Jan 2006 15:04:05 zzz\")"); + + assert.sameValue(Date.parse("Mon, 02 Jan 2006 15:04:05 ZZZ"), NaN, + "Date.parse(\"Mon, 02 Jan 2006 15:04:05 ZZZ\")"); + + var minDateStr = "-271821-04-20T00:00:00.000Z"; + var minDate = new Date(-8640000000000000); + + assert.sameValue(minDate.toISOString(), minDateStr, "minDateStr"); + assert.sameValue(Date.parse(minDateStr), minDate.valueOf(), "parse minDateStr"); + + var maxDateStr = "+275760-09-13T00:00:00.000Z"; + var maxDate = new Date(8640000000000000); + + assert.sameValue(maxDate.toISOString(), maxDateStr, "maxDateStr"); + assert.sameValue(Date.parse(maxDateStr), maxDate.valueOf(), "parse maxDateStr"); + + var belowRange = "-271821-04-19T23:59:59.999Z"; + var aboveRange = "+275760-09-13T00:00:00.001Z"; + + assert.sameValue(Date.parse(belowRange), NaN, "parse below minimum time value"); + assert.sameValue(Date.parse(aboveRange), NaN, "parse above maximum time value"); + ` + + l := time.Local + defer func() { + time.Local = l + }() + var err error + time.Local, err = time.LoadLocation("America/New_York") + if err != nil { + t.Fatal(err) + } + + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestDateMaxValues(t *testing.T) { + const SCRIPT = ` + assert.sameValue((new Date(0)).setUTCMilliseconds(8.64e15), 8.64e15); + assert.sameValue((new Date(0)).setUTCSeconds(8640000000000), 8.64e15); + assert.sameValue((new Date(0)).setUTCMilliseconds(-8.64e15), -8.64e15); + assert.sameValue((new Date(0)).setUTCSeconds(-8640000000000), -8.64e15); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestDateExport(t *testing.T) { + vm := New() + res, err := vm.RunString(`new Date(1000)`) + if err != nil { + t.Fatal(err) + } + exp := res.Export() + if d, ok := exp.(time.Time); ok { + if d.UnixNano()/1e6 != 1000 { + t.Fatalf("Invalid exported date: %v", d) + } + if loc := d.Location(); loc != time.Local { + t.Fatalf("Invalid timezone: %v", loc) + } + } else { + t.Fatalf("Invalid export type: %T", exp) + } +} + +func TestDateToJSON(t *testing.T) { + const SCRIPT = ` + Date.prototype.toJSON.call({ toISOString: function () { return 1; } }) + ` + testScript(SCRIPT, intToValue(1), t) +} + +func TestDateExportType(t *testing.T) { + vm := New() + v, err := vm.RunString(`new Date()`) + if err != nil { + t.Fatal(err) + } + if typ := v.ExportType(); typ != typeTime { + t.Fatal(typ) + } +} diff --git a/goja/destruct.go b/goja/destruct.go new file mode 100644 index 0000000..66792dc --- /dev/null +++ b/goja/destruct.go @@ -0,0 +1,300 @@ +package goja + +import ( + "github.com/dop251/goja/unistring" + "reflect" +) + +type destructKeyedSource struct { + r *Runtime + wrapped Value + usedKeys map[Value]struct{} +} + +func newDestructKeyedSource(r *Runtime, wrapped Value) *destructKeyedSource { + return &destructKeyedSource{ + r: r, + wrapped: wrapped, + } +} + +func (r *Runtime) newDestructKeyedSource(wrapped Value) *Object { + return &Object{ + runtime: r, + self: newDestructKeyedSource(r, wrapped), + } +} + +func (d *destructKeyedSource) w() objectImpl { + return d.wrapped.ToObject(d.r).self +} + +func (d *destructKeyedSource) recordKey(key Value) { + if d.usedKeys == nil { + d.usedKeys = make(map[Value]struct{}) + } + d.usedKeys[key] = struct{}{} +} + +func (d *destructKeyedSource) sortLen() int { + return d.w().sortLen() +} + +func (d *destructKeyedSource) sortGet(i int) Value { + return d.w().sortGet(i) +} + +func (d *destructKeyedSource) swap(i int, i2 int) { + d.w().swap(i, i2) +} + +func (d *destructKeyedSource) className() string { + return d.w().className() +} + +func (d *destructKeyedSource) typeOf() String { + return d.w().typeOf() +} + +func (d *destructKeyedSource) getStr(p unistring.String, receiver Value) Value { + d.recordKey(stringValueFromRaw(p)) + return d.w().getStr(p, receiver) +} + +func (d *destructKeyedSource) getIdx(p valueInt, receiver Value) Value { + d.recordKey(p.toString()) + return d.w().getIdx(p, receiver) +} + +func (d *destructKeyedSource) getSym(p *Symbol, receiver Value) Value { + d.recordKey(p) + return d.w().getSym(p, receiver) +} + +func (d *destructKeyedSource) getOwnPropStr(u unistring.String) Value { + d.recordKey(stringValueFromRaw(u)) + return d.w().getOwnPropStr(u) +} + +func (d *destructKeyedSource) getOwnPropIdx(v valueInt) Value { + d.recordKey(v.toString()) + return d.w().getOwnPropIdx(v) +} + +func (d *destructKeyedSource) getOwnPropSym(symbol *Symbol) Value { + d.recordKey(symbol) + return d.w().getOwnPropSym(symbol) +} + +func (d *destructKeyedSource) setOwnStr(p unistring.String, v Value, throw bool) bool { + return d.w().setOwnStr(p, v, throw) +} + +func (d *destructKeyedSource) setOwnIdx(p valueInt, v Value, throw bool) bool { + return d.w().setOwnIdx(p, v, throw) +} + +func (d *destructKeyedSource) setOwnSym(p *Symbol, v Value, throw bool) bool { + return d.w().setOwnSym(p, v, throw) +} + +func (d *destructKeyedSource) setForeignStr(p unistring.String, v, receiver Value, throw bool) (res bool, handled bool) { + return d.w().setForeignStr(p, v, receiver, throw) +} + +func (d *destructKeyedSource) setForeignIdx(p valueInt, v, receiver Value, throw bool) (res bool, handled bool) { + return d.w().setForeignIdx(p, v, receiver, throw) +} + +func (d *destructKeyedSource) setForeignSym(p *Symbol, v, receiver Value, throw bool) (res bool, handled bool) { + return d.w().setForeignSym(p, v, receiver, throw) +} + +func (d *destructKeyedSource) hasPropertyStr(u unistring.String) bool { + return d.w().hasPropertyStr(u) +} + +func (d *destructKeyedSource) hasPropertyIdx(idx valueInt) bool { + return d.w().hasPropertyIdx(idx) +} + +func (d *destructKeyedSource) hasPropertySym(s *Symbol) bool { + return d.w().hasPropertySym(s) +} + +func (d *destructKeyedSource) hasOwnPropertyStr(u unistring.String) bool { + return d.w().hasOwnPropertyStr(u) +} + +func (d *destructKeyedSource) hasOwnPropertyIdx(v valueInt) bool { + return d.w().hasOwnPropertyIdx(v) +} + +func (d *destructKeyedSource) hasOwnPropertySym(s *Symbol) bool { + return d.w().hasOwnPropertySym(s) +} + +func (d *destructKeyedSource) defineOwnPropertyStr(name unistring.String, desc PropertyDescriptor, throw bool) bool { + return d.w().defineOwnPropertyStr(name, desc, throw) +} + +func (d *destructKeyedSource) defineOwnPropertyIdx(name valueInt, desc PropertyDescriptor, throw bool) bool { + return d.w().defineOwnPropertyIdx(name, desc, throw) +} + +func (d *destructKeyedSource) defineOwnPropertySym(name *Symbol, desc PropertyDescriptor, throw bool) bool { + return d.w().defineOwnPropertySym(name, desc, throw) +} + +func (d *destructKeyedSource) deleteStr(name unistring.String, throw bool) bool { + return d.w().deleteStr(name, throw) +} + +func (d *destructKeyedSource) deleteIdx(idx valueInt, throw bool) bool { + return d.w().deleteIdx(idx, throw) +} + +func (d *destructKeyedSource) deleteSym(s *Symbol, throw bool) bool { + return d.w().deleteSym(s, throw) +} + +func (d *destructKeyedSource) assertCallable() (call func(FunctionCall) Value, ok bool) { + return d.w().assertCallable() +} + +func (d *destructKeyedSource) vmCall(vm *vm, n int) { + d.w().vmCall(vm, n) +} + +func (d *destructKeyedSource) assertConstructor() func(args []Value, newTarget *Object) *Object { + return d.w().assertConstructor() +} + +func (d *destructKeyedSource) proto() *Object { + return d.w().proto() +} + +func (d *destructKeyedSource) setProto(proto *Object, throw bool) bool { + return d.w().setProto(proto, throw) +} + +func (d *destructKeyedSource) hasInstance(v Value) bool { + return d.w().hasInstance(v) +} + +func (d *destructKeyedSource) isExtensible() bool { + return d.w().isExtensible() +} + +func (d *destructKeyedSource) preventExtensions(throw bool) bool { + return d.w().preventExtensions(throw) +} + +type destructKeyedSourceIter struct { + d *destructKeyedSource + wrapped iterNextFunc +} + +func (i *destructKeyedSourceIter) next() (propIterItem, iterNextFunc) { + for { + item, next := i.wrapped() + if next == nil { + return item, nil + } + i.wrapped = next + if _, exists := i.d.usedKeys[item.name]; !exists { + return item, i.next + } + } +} + +func (d *destructKeyedSource) iterateStringKeys() iterNextFunc { + return (&destructKeyedSourceIter{ + d: d, + wrapped: d.w().iterateStringKeys(), + }).next +} + +func (d *destructKeyedSource) iterateSymbols() iterNextFunc { + return (&destructKeyedSourceIter{ + d: d, + wrapped: d.w().iterateSymbols(), + }).next +} + +func (d *destructKeyedSource) iterateKeys() iterNextFunc { + return (&destructKeyedSourceIter{ + d: d, + wrapped: d.w().iterateKeys(), + }).next +} + +func (d *destructKeyedSource) export(ctx *objectExportCtx) interface{} { + return d.w().export(ctx) +} + +func (d *destructKeyedSource) exportType() reflect.Type { + return d.w().exportType() +} + +func (d *destructKeyedSource) exportToMap(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + return d.w().exportToMap(dst, typ, ctx) +} + +func (d *destructKeyedSource) exportToArrayOrSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + return d.w().exportToArrayOrSlice(dst, typ, ctx) +} + +func (d *destructKeyedSource) equal(impl objectImpl) bool { + return d.w().equal(impl) +} + +func (d *destructKeyedSource) stringKeys(all bool, accum []Value) []Value { + var next iterNextFunc + if all { + next = d.iterateStringKeys() + } else { + next = (&enumerableIter{ + o: d.wrapped.ToObject(d.r), + wrapped: d.iterateStringKeys(), + }).next + } + for item, next := next(); next != nil; item, next = next() { + accum = append(accum, item.name) + } + return accum +} + +func (d *destructKeyedSource) filterUsedKeys(keys []Value) []Value { + k := 0 + for i, key := range keys { + if _, exists := d.usedKeys[key]; exists { + continue + } + if k != i { + keys[k] = key + } + k++ + } + return keys[:k] +} + +func (d *destructKeyedSource) symbols(all bool, accum []Value) []Value { + return d.filterUsedKeys(d.w().symbols(all, accum)) +} + +func (d *destructKeyedSource) keys(all bool, accum []Value) []Value { + return d.filterUsedKeys(d.w().keys(all, accum)) +} + +func (d *destructKeyedSource) _putProp(name unistring.String, value Value, writable, enumerable, configurable bool) Value { + return d.w()._putProp(name, value, writable, enumerable, configurable) +} + +func (d *destructKeyedSource) _putSym(s *Symbol, prop Value) { + d.w()._putSym(s, prop) +} + +func (d *destructKeyedSource) getPrivateEnv(typ *privateEnvType, create bool) *privateElements { + return d.w().getPrivateEnv(typ, create) +} diff --git a/goja/extract_failed_tests.sh b/goja/extract_failed_tests.sh new file mode 100755 index 0000000..c32513f --- /dev/null +++ b/goja/extract_failed_tests.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +sed -En 's/^.*FAIL: TestTC39\/tc39\/(test\/.*.js).*$/"\1": true,/p' diff --git a/goja/file/README.markdown b/goja/file/README.markdown new file mode 100644 index 0000000..e9228c2 --- /dev/null +++ b/goja/file/README.markdown @@ -0,0 +1,110 @@ +# file +-- + import "github.com/dop251/goja/file" + +Package file encapsulates the file abstractions used by the ast & parser. + +## Usage + +#### type File + +```go +type File struct { +} +``` + + +#### func NewFile + +```go +func NewFile(filename, src string, base int) *File +``` + +#### func (*File) Base + +```go +func (fl *File) Base() int +``` + +#### func (*File) Name + +```go +func (fl *File) Name() string +``` + +#### func (*File) Source + +```go +func (fl *File) Source() string +``` + +#### type FileSet + +```go +type FileSet struct { +} +``` + +A FileSet represents a set of source files. + +#### func (*FileSet) AddFile + +```go +func (self *FileSet) AddFile(filename, src string) int +``` +AddFile adds a new file with the given filename and src. + +This an internal method, but exported for cross-package use. + +#### func (*FileSet) File + +```go +func (self *FileSet) File(idx Idx) *File +``` + +#### func (*FileSet) Position + +```go +func (self *FileSet) Position(idx Idx) *Position +``` +Position converts an Idx in the FileSet into a Position. + +#### type Idx + +```go +type Idx int +``` + +Idx is a compact encoding of a source position within a file set. It can be +converted into a Position for a more convenient, but much larger, +representation. + +#### type Position + +```go +type Position struct { + Filename string // The filename where the error occurred, if any + Offset int // The src offset + Line int // The line number, starting at 1 + Column int // The column number, starting at 1 (The character count) + +} +``` + +Position describes an arbitrary source position including the filename, line, +and column location. + +#### func (*Position) String + +```go +func (self *Position) String() string +``` +String returns a string in one of several forms: + + file:line:column A valid position with filename + line:column A valid position without filename + file An invalid position with filename + - An invalid position without filename + +-- +**godocdown** http://github.com/robertkrimen/godocdown diff --git a/goja/file/file.go b/goja/file/file.go new file mode 100644 index 0000000..2d9ed7c --- /dev/null +++ b/goja/file/file.go @@ -0,0 +1,234 @@ +// Package file encapsulates the file abstractions used by the ast & parser. +package file + +import ( + "fmt" + "net/url" + "path" + "sort" + "strings" + "sync" + + "github.com/go-sourcemap/sourcemap" +) + +// Idx is a compact encoding of a source position within a file set. +// It can be converted into a Position for a more convenient, but much +// larger, representation. +type Idx int + +// Position describes an arbitrary source position +// including the filename, line, and column location. +type Position struct { + Filename string // The filename where the error occurred, if any + Line int // The line number, starting at 1 + Column int // The column number, starting at 1 (The character count) + +} + +// A Position is valid if the line number is > 0. + +func (self *Position) isValid() bool { + return self.Line > 0 +} + +// String returns a string in one of several forms: +// +// file:line:column A valid position with filename +// line:column A valid position without filename +// file An invalid position with filename +// - An invalid position without filename +func (self Position) String() string { + str := self.Filename + if self.isValid() { + if str != "" { + str += ":" + } + str += fmt.Sprintf("%d:%d", self.Line, self.Column) + } + if str == "" { + str = "-" + } + return str +} + +// FileSet + +// A FileSet represents a set of source files. +type FileSet struct { + files []*File + last *File +} + +// AddFile adds a new file with the given filename and src. +// +// This an internal method, but exported for cross-package use. +func (self *FileSet) AddFile(filename, src string) int { + base := self.nextBase() + file := &File{ + name: filename, + src: src, + base: base, + } + self.files = append(self.files, file) + self.last = file + return base +} + +func (self *FileSet) nextBase() int { + if self.last == nil { + return 1 + } + return self.last.base + len(self.last.src) + 1 +} + +func (self *FileSet) File(idx Idx) *File { + for _, file := range self.files { + if idx <= Idx(file.base+len(file.src)) { + return file + } + } + return nil +} + +// Position converts an Idx in the FileSet into a Position. +func (self *FileSet) Position(idx Idx) Position { + for _, file := range self.files { + if idx <= Idx(file.base+len(file.src)) { + return file.Position(int(idx) - file.base) + } + } + return Position{} +} + +type File struct { + mu sync.Mutex + name string + src string + base int // This will always be 1 or greater + sourceMap *sourcemap.Consumer + lineOffsets []int + lastScannedOffset int +} + +func NewFile(filename, src string, base int) *File { + return &File{ + name: filename, + src: src, + base: base, + } +} + +func (fl *File) Name() string { + return fl.name +} + +func (fl *File) Source() string { + return fl.src +} + +func (fl *File) Base() int { + return fl.base +} + +func (fl *File) SetSourceMap(m *sourcemap.Consumer) { + fl.sourceMap = m +} + +func (fl *File) Position(offset int) Position { + var line int + var lineOffsets []int + fl.mu.Lock() + if offset > fl.lastScannedOffset { + line = fl.scanTo(offset) + lineOffsets = fl.lineOffsets + fl.mu.Unlock() + } else { + lineOffsets = fl.lineOffsets + fl.mu.Unlock() + line = sort.Search(len(lineOffsets), func(x int) bool { return lineOffsets[x] > offset }) - 1 + } + + var lineStart int + if line >= 0 { + lineStart = lineOffsets[line] + } + + row := line + 2 + col := offset - lineStart + 1 + + if fl.sourceMap != nil { + if source, _, row, col, ok := fl.sourceMap.Source(row, col); ok { + sourceUrlStr := source + sourceURL := ResolveSourcemapURL(fl.Name(), source) + if sourceURL != nil { + sourceUrlStr = sourceURL.String() + } + + return Position{ + Filename: sourceUrlStr, + Line: row, + Column: col, + } + } + } + + return Position{ + Filename: fl.name, + Line: row, + Column: col, + } +} + +func ResolveSourcemapURL(basename, source string) *url.URL { + // if the url is absolute(has scheme) there is nothing to do + smURL, err := url.Parse(strings.TrimSpace(source)) + if err == nil && !smURL.IsAbs() { + baseURL, err1 := url.Parse(strings.TrimSpace(basename)) + if err1 == nil && path.IsAbs(baseURL.Path) { + smURL = baseURL.ResolveReference(smURL) + } else { + // pathological case where both are not absolute paths and using Resolve + // as above will produce an absolute one + smURL, _ = url.Parse(path.Join(path.Dir(basename), smURL.Path)) + } + } + return smURL +} + +func findNextLineStart(s string) int { + for pos, ch := range s { + switch ch { + case '\r': + if pos < len(s)-1 && s[pos+1] == '\n' { + return pos + 2 + } + return pos + 1 + case '\n': + return pos + 1 + case '\u2028', '\u2029': + return pos + 3 + } + } + return -1 +} + +func (fl *File) scanTo(offset int) int { + o := fl.lastScannedOffset + for o < offset { + p := findNextLineStart(fl.src[o:]) + if p == -1 { + fl.lastScannedOffset = len(fl.src) + return len(fl.lineOffsets) - 1 + } + o = o + p + fl.lineOffsets = append(fl.lineOffsets, o) + } + fl.lastScannedOffset = o + + if o == offset { + return len(fl.lineOffsets) - 1 + } + + return len(fl.lineOffsets) - 2 +} diff --git a/goja/file/file_test.go b/goja/file/file_test.go new file mode 100644 index 0000000..c49c312 --- /dev/null +++ b/goja/file/file_test.go @@ -0,0 +1,75 @@ +package file + +import ( + "testing" +) + +func TestPosition(t *testing.T) { + const SRC = `line1 +line2 +line3` + f := NewFile("", SRC, 0) + + tests := []struct { + offset int + line int + col int + }{ + {0, 1, 1}, + {2, 1, 3}, + {2, 1, 3}, + {6, 2, 1}, + {7, 2, 2}, + {12, 3, 1}, + {12, 3, 1}, + {13, 3, 2}, + {13, 3, 2}, + {16, 3, 5}, + {17, 3, 6}, + } + + for i, test := range tests { + if p := f.Position(test.offset); p.Line != test.line || p.Column != test.col { + t.Fatalf("%d. Line: %d, col: %d", i, p.Line, p.Column) + } + } +} + +func TestFileConcurrency(t *testing.T) { + const SRC = `line1 +line2 +line3` + f := NewFile("", SRC, 0) + go func() { + f.Position(12) + }() + f.Position(2) +} + +func TestGetSourceFilename(t *testing.T) { + tests := []struct { + source, basename, result string + }{ + {"test.js", "base.js", "test.js"}, + {"test.js", "../base.js", "../test.js"}, + {"test.js", "/somewhere/base.js", "/somewhere/test.js"}, + {"/test.js", "/somewhere/base.js", "/test.js"}, + {"/test.js", "file:///somewhere/base.js", "file:///test.js"}, + {"file:///test.js", "base.js", "file:///test.js"}, + {"file:///test.js", "/somwehere/base.js", "file:///test.js"}, + {"file:///test.js", "file:///somewhere/base.js", "file:///test.js"}, + {"../test.js", "/somewhere/else/base.js", "/somewhere/test.js"}, + {"../test.js", "file:///somewhere/else/base.js", "file:///somewhere/test.js"}, + {"../test.js", "https://example.com/somewhere/else/base.js", "https://example.com/somewhere/test.js"}, + {"\ntest.js", "base123.js", "test.js"}, + {"\rtest2.js\t\n ", "base123.js", "test2.js"}, + // TODO find something that won't parse + } + for _, test := range tests { + resultURL := ResolveSourcemapURL(test.basename, test.source) + result := resultURL.String() + if result != test.result { + t.Fatalf("source: %q, basename %q produced %q instead of %q", test.source, test.basename, result, test.result) + } + } +} diff --git a/goja/ftoa/LICENSE_LUCENE b/goja/ftoa/LICENSE_LUCENE new file mode 100644 index 0000000..c8da489 --- /dev/null +++ b/goja/ftoa/LICENSE_LUCENE @@ -0,0 +1,21 @@ +Copyright (C) 1998, 1999 by Lucent Technologies +All Rights Reserved + +Permission to use, copy, modify, and distribute this software and +its documentation for any purpose and without fee is hereby +granted, provided that the above copyright notice appear in all +copies and that both that the copyright notice and this +permission notice and warranty disclaimer appear in supporting +documentation, and that the name of Lucent or any of its entities +not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +LUCENT DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. +IN NO EVENT SHALL LUCENT OR ANY OF ITS ENTITIES BE LIABLE FOR ANY +SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER +IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. diff --git a/goja/ftoa/common.go b/goja/ftoa/common.go new file mode 100644 index 0000000..b59a93c --- /dev/null +++ b/goja/ftoa/common.go @@ -0,0 +1,150 @@ +/* +Package ftoa provides ECMAScript-compliant floating point number conversion to string. + +It contains code ported from Rhino (https://github.com/mozilla/rhino/blob/master/src/org/mozilla/javascript/DToA.java) +as well as from the original code by David M. Gay. + +See LICENSE_LUCENE for the original copyright message and disclaimer. +*/ +package ftoa + +import ( + "math" +) + +const ( + frac_mask = 0xfffff + exp_shift = 20 + exp_msk1 = 0x100000 + + exp_shiftL = 52 + exp_mask_shifted = 0x7ff + frac_maskL = 0xfffffffffffff + exp_msk1L = 0x10000000000000 + exp_shift1 = 20 + exp_mask = 0x7ff00000 + bias = 1023 + p = 53 + bndry_mask = 0xfffff + log2P = 1 +) + +func lo0bits(x uint32) (k int) { + + if (x & 7) != 0 { + if (x & 1) != 0 { + return 0 + } + if (x & 2) != 0 { + return 1 + } + return 2 + } + if (x & 0xffff) == 0 { + k = 16 + x >>= 16 + } + if (x & 0xff) == 0 { + k += 8 + x >>= 8 + } + if (x & 0xf) == 0 { + k += 4 + x >>= 4 + } + if (x & 0x3) == 0 { + k += 2 + x >>= 2 + } + if (x & 1) == 0 { + k++ + x >>= 1 + if (x & 1) == 0 { + return 32 + } + } + return +} + +func hi0bits(x uint32) (k int) { + + if (x & 0xffff0000) == 0 { + k = 16 + x <<= 16 + } + if (x & 0xff000000) == 0 { + k += 8 + x <<= 8 + } + if (x & 0xf0000000) == 0 { + k += 4 + x <<= 4 + } + if (x & 0xc0000000) == 0 { + k += 2 + x <<= 2 + } + if (x & 0x80000000) == 0 { + k++ + if (x & 0x40000000) == 0 { + return 32 + } + } + return +} + +func stuffBits(bits []byte, offset int, val uint32) { + bits[offset] = byte(val >> 24) + bits[offset+1] = byte(val >> 16) + bits[offset+2] = byte(val >> 8) + bits[offset+3] = byte(val) +} + +func d2b(d float64, b []byte) (e, bits int, dblBits []byte) { + dBits := math.Float64bits(d) + d0 := uint32(dBits >> 32) + d1 := uint32(dBits) + + z := d0 & frac_mask + d0 &= 0x7fffffff /* clear sign bit, which we ignore */ + + var de, k, i int + if de = int(d0 >> exp_shift); de != 0 { + z |= exp_msk1 + } + + y := d1 + if y != 0 { + dblBits = b[:8] + k = lo0bits(y) + y >>= k + if k != 0 { + stuffBits(dblBits, 4, y|z<<(32-k)) + z >>= k + } else { + stuffBits(dblBits, 4, y) + } + stuffBits(dblBits, 0, z) + if z != 0 { + i = 2 + } else { + i = 1 + } + } else { + dblBits = b[:4] + k = lo0bits(z) + z >>= k + stuffBits(dblBits, 0, z) + k += 32 + i = 1 + } + + if de != 0 { + e = de - bias - (p - 1) + k + bits = p - k + } else { + e = de - bias - (p - 1) + 1 + k + bits = 32*i - hi0bits(z) + } + return +} diff --git a/goja/ftoa/ftoa.go b/goja/ftoa/ftoa.go new file mode 100644 index 0000000..dd266ec --- /dev/null +++ b/goja/ftoa/ftoa.go @@ -0,0 +1,699 @@ +package ftoa + +import ( + "math" + "math/big" +) + +const ( + exp_11 = 0x3ff00000 + frac_mask1 = 0xfffff + bletch = 0x10 + quick_max = 14 + int_max = 14 +) + +var ( + tens = [...]float64{ + 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, + 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, + 1e20, 1e21, 1e22, + } + + bigtens = [...]float64{1e16, 1e32, 1e64, 1e128, 1e256} + + big5 = big.NewInt(5) + big10 = big.NewInt(10) + + p05 = []*big.Int{big5, big.NewInt(25), big.NewInt(125)} + pow5Cache [7]*big.Int + + dtoaModes = []int{ + ModeStandard: 0, + ModeStandardExponential: 0, + ModeFixed: 3, + ModeExponential: 2, + ModePrecision: 2, + } +) + +/* +d must be > 0 and must not be Inf + +mode: + + 0 ==> shortest string that yields d when read in + and rounded to nearest. + 1 ==> like 0, but with Steele & White stopping rule; + e.g. with IEEE P754 arithmetic , mode 0 gives + 1e23 whereas mode 1 gives 9.999999999999999e22. + 2 ==> max(1,ndigits) significant digits. This gives a + return value similar to that of ecvt, except + that trailing zeros are suppressed. + 3 ==> through ndigits past the decimal point. This + gives a return value similar to that from fcvt, + except that trailing zeros are suppressed, and + ndigits can be negative. + 4,5 ==> similar to 2 and 3, respectively, but (in + round-nearest mode) with the tests of mode 0 to + possibly return a shorter string that rounds to d. + With IEEE arithmetic and compilation with + -DHonor_FLT_ROUNDS, modes 4 and 5 behave the same + as modes 2 and 3 when FLT_ROUNDS != 1. + 6-9 ==> Debugging modes similar to mode - 4: don't try + fast floating-point estimate (if applicable). + + Values of mode other than 0-9 are treated as mode 0. +*/ +func ftoa(d float64, mode int, biasUp bool, ndigits int, buf []byte) ([]byte, int) { + startPos := len(buf) + dblBits := make([]byte, 0, 8) + be, bbits, dblBits := d2b(d, dblBits) + + dBits := math.Float64bits(d) + word0 := uint32(dBits >> 32) + word1 := uint32(dBits) + + i := int((word0 >> exp_shift1) & (exp_mask >> exp_shift1)) + var d2 float64 + var denorm bool + if i != 0 { + d2 = setWord0(d, (word0&frac_mask1)|exp_11) + i -= bias + denorm = false + } else { + /* d is denormalized */ + i = bbits + be + (bias + (p - 1) - 1) + var x uint64 + if i > 32 { + x = uint64(word0)<<(64-i) | uint64(word1)>>(i-32) + } else { + x = uint64(word1) << (32 - i) + } + d2 = setWord0(float64(x), uint32((x>>32)-31*exp_mask)) + i -= (bias + (p - 1) - 1) + 1 + denorm = true + } + /* At this point d = f*2^i, where 1 <= f < 2. d2 is an approximation of f. */ + ds := (d2-1.5)*0.289529654602168 + 0.1760912590558 + float64(i)*0.301029995663981 + k := int(ds) + if ds < 0.0 && ds != float64(k) { + k-- /* want k = floor(ds) */ + } + k_check := true + if k >= 0 && k < len(tens) { + if d < tens[k] { + k-- + } + k_check = false + } + /* At this point floor(log10(d)) <= k <= floor(log10(d))+1. + If k_check is zero, we're guaranteed that k = floor(log10(d)). */ + j := bbits - i - 1 + var b2, s2, b5, s5 int + /* At this point d = b/2^j, where b is an odd integer. */ + if j >= 0 { + b2 = 0 + s2 = j + } else { + b2 = -j + s2 = 0 + } + if k >= 0 { + b5 = 0 + s5 = k + s2 += k + } else { + b2 -= k + b5 = -k + s5 = 0 + } + /* At this point d/10^k = (b * 2^b2 * 5^b5) / (2^s2 * 5^s5), where b is an odd integer, + b2 >= 0, b5 >= 0, s2 >= 0, and s5 >= 0. */ + if mode < 0 || mode > 9 { + mode = 0 + } + try_quick := true + if mode > 5 { + mode -= 4 + try_quick = false + } + leftright := true + var ilim, ilim1 int + switch mode { + case 0, 1: + ilim, ilim1 = -1, -1 + ndigits = 0 + case 2: + leftright = false + fallthrough + case 4: + if ndigits <= 0 { + ndigits = 1 + } + ilim, ilim1 = ndigits, ndigits + case 3: + leftright = false + fallthrough + case 5: + i = ndigits + k + 1 + ilim = i + ilim1 = i - 1 + } + /* ilim is the maximum number of significant digits we want, based on k and ndigits. */ + /* ilim1 is the maximum number of significant digits we want, based on k and ndigits, + when it turns out that k was computed too high by one. */ + fast_failed := false + if ilim >= 0 && ilim <= quick_max && try_quick { + + /* Try to get by with floating-point arithmetic. */ + + i = 0 + d2 = d + k0 := k + ilim0 := ilim + ieps := 2 /* conservative */ + /* Divide d by 10^k, keeping track of the roundoff error and avoiding overflows. */ + if k > 0 { + ds = tens[k&0xf] + j = k >> 4 + if (j & bletch) != 0 { + /* prevent overflows */ + j &= bletch - 1 + d /= bigtens[len(bigtens)-1] + ieps++ + } + for ; j != 0; i++ { + if (j & 1) != 0 { + ieps++ + ds *= bigtens[i] + } + j >>= 1 + } + d /= ds + } else if j1 := -k; j1 != 0 { + d *= tens[j1&0xf] + for j = j1 >> 4; j != 0; i++ { + if (j & 1) != 0 { + ieps++ + d *= bigtens[i] + } + j >>= 1 + } + } + /* Check that k was computed correctly. */ + if k_check && d < 1.0 && ilim > 0 { + if ilim1 <= 0 { + fast_failed = true + } else { + ilim = ilim1 + k-- + d *= 10. + ieps++ + } + } + /* eps bounds the cumulative error. */ + eps := float64(ieps)*d + 7.0 + eps = setWord0(eps, _word0(eps)-(p-1)*exp_msk1) + if ilim == 0 { + d -= 5.0 + if d > eps { + buf = append(buf, '1') + k++ + return buf, k + 1 + } + if d < -eps { + buf = append(buf, '0') + return buf, 1 + } + fast_failed = true + } + if !fast_failed { + fast_failed = true + if leftright { + /* Use Steele & White method of only + * generating digits needed. + */ + eps = 0.5/tens[ilim-1] - eps + for i = 0; ; { + l := int64(d) + d -= float64(l) + buf = append(buf, byte('0'+l)) + if d < eps { + return buf, k + 1 + } + if 1.0-d < eps { + buf, k = bumpUp(buf, k) + return buf, k + 1 + } + i++ + if i >= ilim { + break + } + eps *= 10.0 + d *= 10.0 + } + } else { + /* Generate ilim digits, then fix them up. */ + eps *= tens[ilim-1] + for i = 1; ; i++ { + l := int64(d) + d -= float64(l) + buf = append(buf, byte('0'+l)) + if i == ilim { + if d > 0.5+eps { + buf, k = bumpUp(buf, k) + return buf, k + 1 + } else if d < 0.5-eps { + buf = stripTrailingZeroes(buf, startPos) + return buf, k + 1 + } + break + } + d *= 10.0 + } + } + } + if fast_failed { + buf = buf[:startPos] + d = d2 + k = k0 + ilim = ilim0 + } + } + + /* Do we have a "small" integer? */ + if be >= 0 && k <= int_max { + /* Yes. */ + ds = tens[k] + if ndigits < 0 && ilim <= 0 { + if ilim < 0 || d < 5*ds || (!biasUp && d == 5*ds) { + buf = buf[:startPos] + buf = append(buf, '0') + return buf, 1 + } + buf = append(buf, '1') + k++ + return buf, k + 1 + } + for i = 1; ; i++ { + l := int64(d / ds) + d -= float64(l) * ds + buf = append(buf, byte('0'+l)) + if i == ilim { + d += d + if (d > ds) || (d == ds && (((l & 1) != 0) || biasUp)) { + buf, k = bumpUp(buf, k) + } + break + } + d *= 10.0 + if d == 0 { + break + } + } + return buf, k + 1 + } + + m2 := b2 + m5 := b5 + var mhi, mlo *big.Int + if leftright { + if mode < 2 { + if denorm { + i = be + (bias + (p - 1) - 1 + 1) + } else { + i = 1 + p - bbits + } + /* i is 1 plus the number of trailing zero bits in d's significand. Thus, + (2^m2 * 5^m5) / (2^(s2+i) * 5^s5) = (1/2 lsb of d)/10^k. */ + } else { + j = ilim - 1 + if m5 >= j { + m5 -= j + } else { + j -= m5 + s5 += j + b5 += j + m5 = 0 + } + i = ilim + if i < 0 { + m2 -= i + i = 0 + } + /* (2^m2 * 5^m5) / (2^(s2+i) * 5^s5) = (1/2 * 10^(1-ilim))/10^k. */ + } + b2 += i + s2 += i + mhi = big.NewInt(1) + /* (mhi * 2^m2 * 5^m5) / (2^s2 * 5^s5) = one-half of last printed (when mode >= 2) or + input (when mode < 2) significant digit, divided by 10^k. */ + } + + /* We still have d/10^k = (b * 2^b2 * 5^b5) / (2^s2 * 5^s5). Reduce common factors in + b2, m2, and s2 without changing the equalities. */ + if m2 > 0 && s2 > 0 { + if m2 < s2 { + i = m2 + } else { + i = s2 + } + b2 -= i + m2 -= i + s2 -= i + } + + b := new(big.Int).SetBytes(dblBits) + /* Fold b5 into b and m5 into mhi. */ + if b5 > 0 { + if leftright { + if m5 > 0 { + pow5mult(mhi, m5) + b.Mul(mhi, b) + } + j = b5 - m5 + if j != 0 { + pow5mult(b, j) + } + } else { + pow5mult(b, b5) + } + } + /* Now we have d/10^k = (b * 2^b2) / (2^s2 * 5^s5) and + (mhi * 2^m2) / (2^s2 * 5^s5) = one-half of last printed or input significant digit, divided by 10^k. */ + + S := big.NewInt(1) + if s5 > 0 { + pow5mult(S, s5) + } + /* Now we have d/10^k = (b * 2^b2) / (S * 2^s2) and + (mhi * 2^m2) / (S * 2^s2) = one-half of last printed or input significant digit, divided by 10^k. */ + + /* Check for special case that d is a normalized power of 2. */ + spec_case := false + if mode < 2 { + if (_word1(d) == 0) && ((_word0(d) & bndry_mask) == 0) && + ((_word0(d) & (exp_mask & (exp_mask << 1))) != 0) { + /* The special case. Here we want to be within a quarter of the last input + significant digit instead of one half of it when the decimal output string's value is less than d. */ + b2 += log2P + s2 += log2P + spec_case = true + } + } + + /* Arrange for convenient computation of quotients: + * shift left if necessary so divisor has 4 leading 0 bits. + * + * Perhaps we should just compute leading 28 bits of S once + * and for all and pass them and a shift to quorem, so it + * can do shifts and ors to compute the numerator for q. + */ + var zz int + if s5 != 0 { + S_bytes := S.Bytes() + var S_hiWord uint32 + for idx := 0; idx < 4; idx++ { + S_hiWord = S_hiWord << 8 + if idx < len(S_bytes) { + S_hiWord |= uint32(S_bytes[idx]) + } + } + zz = 32 - hi0bits(S_hiWord) + } else { + zz = 1 + } + i = (zz + s2) & 0x1f + if i != 0 { + i = 32 - i + } + /* i is the number of leading zero bits in the most significant word of S*2^s2. */ + if i > 4 { + i -= 4 + b2 += i + m2 += i + s2 += i + } else if i < 4 { + i += 28 + b2 += i + m2 += i + s2 += i + } + /* Now S*2^s2 has exactly four leading zero bits in its most significant word. */ + if b2 > 0 { + b = b.Lsh(b, uint(b2)) + } + if s2 > 0 { + S.Lsh(S, uint(s2)) + } + /* Now we have d/10^k = b/S and + (mhi * 2^m2) / S = maximum acceptable error, divided by 10^k. */ + if k_check { + if b.Cmp(S) < 0 { + k-- + b.Mul(b, big10) /* we botched the k estimate */ + if leftright { + mhi.Mul(mhi, big10) + } + ilim = ilim1 + } + } + /* At this point 1 <= d/10^k = b/S < 10. */ + + if ilim <= 0 && mode > 2 { + /* We're doing fixed-mode output and d is less than the minimum nonzero output in this mode. + Output either zero or the minimum nonzero output depending on which is closer to d. */ + if ilim >= 0 { + i = b.Cmp(S.Mul(S, big5)) + } + if ilim < 0 || i < 0 || i == 0 && !biasUp { + /* Always emit at least one digit. If the number appears to be zero + using the current mode, then emit one '0' digit and set decpt to 1. */ + buf = buf[:startPos] + buf = append(buf, '0') + return buf, 1 + } + buf = append(buf, '1') + k++ + return buf, k + 1 + } + + var dig byte + if leftright { + if m2 > 0 { + mhi.Lsh(mhi, uint(m2)) + } + + /* Compute mlo -- check for special case + * that d is a normalized power of 2. + */ + + mlo = mhi + if spec_case { + mhi = mlo + mhi = new(big.Int).Lsh(mhi, log2P) + } + /* mlo/S = maximum acceptable error, divided by 10^k, if the output is less than d. */ + /* mhi/S = maximum acceptable error, divided by 10^k, if the output is greater than d. */ + var z, delta big.Int + for i = 1; ; i++ { + z.DivMod(b, S, b) + dig = byte(z.Int64() + '0') + /* Do we yet have the shortest decimal string + * that will round to d? + */ + j = b.Cmp(mlo) + /* j is b/S compared with mlo/S. */ + delta.Sub(S, mhi) + var j1 int + if delta.Sign() <= 0 { + j1 = 1 + } else { + j1 = b.Cmp(&delta) + } + /* j1 is b/S compared with 1 - mhi/S. */ + if (j1 == 0) && (mode == 0) && ((_word1(d) & 1) == 0) { + if dig == '9' { + var flag bool + buf = append(buf, '9') + if buf, flag = roundOff(buf, startPos); flag { + k++ + buf = append(buf, '1') + } + return buf, k + 1 + } + if j > 0 { + dig++ + } + buf = append(buf, dig) + return buf, k + 1 + } + if (j < 0) || ((j == 0) && (mode == 0) && ((_word1(d) & 1) == 0)) { + if j1 > 0 { + /* Either dig or dig+1 would work here as the least significant decimal digit. + Use whichever would produce a decimal value closer to d. */ + b.Lsh(b, 1) + j1 = b.Cmp(S) + if (j1 > 0) || (j1 == 0 && (((dig & 1) == 1) || biasUp)) { + dig++ + if dig == '9' { + buf = append(buf, '9') + buf, flag := roundOff(buf, startPos) + if flag { + k++ + buf = append(buf, '1') + } + return buf, k + 1 + } + } + } + buf = append(buf, dig) + return buf, k + 1 + } + if j1 > 0 { + if dig == '9' { /* possible if i == 1 */ + buf = append(buf, '9') + buf, flag := roundOff(buf, startPos) + if flag { + k++ + buf = append(buf, '1') + } + return buf, k + 1 + } + buf = append(buf, dig+1) + return buf, k + 1 + } + buf = append(buf, dig) + if i == ilim { + break + } + b.Mul(b, big10) + if mlo == mhi { + mhi.Mul(mhi, big10) + } else { + mlo.Mul(mlo, big10) + mhi.Mul(mhi, big10) + } + } + } else { + var z big.Int + for i = 1; ; i++ { + z.DivMod(b, S, b) + dig = byte(z.Int64() + '0') + buf = append(buf, dig) + if i >= ilim { + break + } + + b.Mul(b, big10) + } + } + /* Round off last digit */ + + b.Lsh(b, 1) + j = b.Cmp(S) + if (j > 0) || (j == 0 && (((dig & 1) == 1) || biasUp)) { + var flag bool + buf, flag = roundOff(buf, startPos) + if flag { + k++ + buf = append(buf, '1') + return buf, k + 1 + } + } else { + buf = stripTrailingZeroes(buf, startPos) + } + + return buf, k + 1 +} + +func bumpUp(buf []byte, k int) ([]byte, int) { + var lastCh byte + stop := 0 + if len(buf) > 0 && buf[0] == '-' { + stop = 1 + } + for { + lastCh = buf[len(buf)-1] + buf = buf[:len(buf)-1] + if lastCh != '9' { + break + } + if len(buf) == stop { + k++ + lastCh = '0' + break + } + } + buf = append(buf, lastCh+1) + return buf, k +} + +func setWord0(d float64, w uint32) float64 { + dBits := math.Float64bits(d) + return math.Float64frombits(uint64(w)<<32 | dBits&0xffffffff) +} + +func _word0(d float64) uint32 { + dBits := math.Float64bits(d) + return uint32(dBits >> 32) +} + +func _word1(d float64) uint32 { + dBits := math.Float64bits(d) + return uint32(dBits) +} + +func stripTrailingZeroes(buf []byte, startPos int) []byte { + bl := len(buf) - 1 + for bl >= startPos && buf[bl] == '0' { + bl-- + } + return buf[:bl+1] +} + +/* Set b = b * 5^k. k must be nonnegative. */ +func pow5mult(b *big.Int, k int) *big.Int { + if k < (1 << (len(pow5Cache) + 2)) { + i := k & 3 + if i != 0 { + b.Mul(b, p05[i-1]) + } + k >>= 2 + i = 0 + for { + if k&1 != 0 { + b.Mul(b, pow5Cache[i]) + } + k >>= 1 + if k == 0 { + break + } + i++ + } + return b + } + return b.Mul(b, new(big.Int).Exp(big5, big.NewInt(int64(k)), nil)) +} + +func roundOff(buf []byte, startPos int) ([]byte, bool) { + i := len(buf) + for i != startPos { + i-- + if buf[i] != '9' { + buf[i]++ + return buf[:i+1], false + } + } + return buf[:startPos], true +} + +func init() { + p := big.NewInt(625) + pow5Cache[0] = p + for i := 1; i < len(pow5Cache); i++ { + p = new(big.Int).Mul(p, p) + pow5Cache[i] = p + } +} diff --git a/goja/ftoa/ftobasestr.go b/goja/ftoa/ftobasestr.go new file mode 100644 index 0000000..9dc9b2d --- /dev/null +++ b/goja/ftoa/ftobasestr.go @@ -0,0 +1,153 @@ +package ftoa + +import ( + "fmt" + "math" + "math/big" + "strconv" + "strings" +) + +const ( + digits = "0123456789abcdefghijklmnopqrstuvwxyz" +) + +func FToBaseStr(num float64, radix int) string { + var negative bool + if num < 0 { + num = -num + negative = true + } + + dfloor := math.Floor(num) + ldfloor := int64(dfloor) + var intDigits string + if dfloor == float64(ldfloor) { + if negative { + ldfloor = -ldfloor + } + intDigits = strconv.FormatInt(ldfloor, radix) + } else { + floorBits := math.Float64bits(num) + exp := int(floorBits>>exp_shiftL) & exp_mask_shifted + var mantissa int64 + if exp == 0 { + mantissa = int64((floorBits & frac_maskL) << 1) + } else { + mantissa = int64((floorBits & frac_maskL) | exp_msk1L) + } + + if negative { + mantissa = -mantissa + } + exp -= 1075 + x := big.NewInt(mantissa) + if exp > 0 { + x.Lsh(x, uint(exp)) + } else if exp < 0 { + x.Rsh(x, uint(-exp)) + } + intDigits = x.Text(radix) + } + + if num == dfloor { + // No fraction part + return intDigits + } else { + /* We have a fraction. */ + var buffer strings.Builder + buffer.WriteString(intDigits) + buffer.WriteByte('.') + df := num - dfloor + + dBits := math.Float64bits(num) + word0 := uint32(dBits >> 32) + word1 := uint32(dBits) + + dblBits := make([]byte, 0, 8) + e, _, dblBits := d2b(df, dblBits) + // JS_ASSERT(e < 0); + /* At this point df = b * 2^e. e must be less than zero because 0 < df < 1. */ + + s2 := -int((word0 >> exp_shift1) & (exp_mask >> exp_shift1)) + if s2 == 0 { + s2 = -1 + } + s2 += bias + p + /* 1/2^s2 = (nextDouble(d) - d)/2 */ + // JS_ASSERT(-s2 < e); + if -s2 >= e { + panic(fmt.Errorf("-s2 >= e: %d, %d", -s2, e)) + } + mlo := big.NewInt(1) + mhi := mlo + if (word1 == 0) && ((word0 & bndry_mask) == 0) && ((word0 & (exp_mask & (exp_mask << 1))) != 0) { + /* The special case. Here we want to be within a quarter of the last input + significant digit instead of one half of it when the output string's value is less than d. */ + s2 += log2P + mhi = big.NewInt(1 << log2P) + } + + b := new(big.Int).SetBytes(dblBits) + b.Lsh(b, uint(e+s2)) + s := big.NewInt(1) + s.Lsh(s, uint(s2)) + /* At this point we have the following: + * s = 2^s2; + * 1 > df = b/2^s2 > 0; + * (d - prevDouble(d))/2 = mlo/2^s2; + * (nextDouble(d) - d)/2 = mhi/2^s2. */ + bigBase := big.NewInt(int64(radix)) + + done := false + m := &big.Int{} + delta := &big.Int{} + for !done { + b.Mul(b, bigBase) + b.DivMod(b, s, m) + digit := byte(b.Int64()) + b, m = m, b + mlo.Mul(mlo, bigBase) + if mlo != mhi { + mhi.Mul(mhi, bigBase) + } + + /* Do we yet have the shortest string that will round to d? */ + j := b.Cmp(mlo) + /* j is b/2^s2 compared with mlo/2^s2. */ + + delta.Sub(s, mhi) + var j1 int + if delta.Sign() <= 0 { + j1 = 1 + } else { + j1 = b.Cmp(delta) + } + /* j1 is b/2^s2 compared with 1 - mhi/2^s2. */ + if j1 == 0 && (word1&1) == 0 { + if j > 0 { + digit++ + } + done = true + } else if j < 0 || (j == 0 && ((word1 & 1) == 0)) { + if j1 > 0 { + /* Either dig or dig+1 would work here as the least significant digit. + Use whichever would produce an output value closer to d. */ + b.Lsh(b, 1) + j1 = b.Cmp(s) + if j1 > 0 { /* The even test (|| (j1 == 0 && (digit & 1))) is not here because it messes up odd base output such as 3.5 in base 3. */ + digit++ + } + } + done = true + } else if j1 > 0 { + digit++ + done = true + } + // JS_ASSERT(digit < (uint32)base); + buffer.WriteByte(digits[digit]) + } + + return buffer.String() + } +} diff --git a/goja/ftoa/ftobasestr_test.go b/goja/ftoa/ftobasestr_test.go new file mode 100644 index 0000000..0a3fecb --- /dev/null +++ b/goja/ftoa/ftobasestr_test.go @@ -0,0 +1,9 @@ +package ftoa + +import "testing" + +func TestFToBaseStr(t *testing.T) { + if s := FToBaseStr(0.8466400793967279, 36); s != "0.uh8u81s3fz" { + t.Fatal(s) + } +} diff --git a/goja/ftoa/ftostr.go b/goja/ftoa/ftostr.go new file mode 100644 index 0000000..a9d2d24 --- /dev/null +++ b/goja/ftoa/ftostr.go @@ -0,0 +1,147 @@ +package ftoa + +import ( + "math" + "strconv" + + "github.com/dop251/goja/ftoa/internal/fast" +) + +type FToStrMode int + +const ( + // Either fixed or exponential format; round-trip + ModeStandard FToStrMode = iota + // Always exponential format; round-trip + ModeStandardExponential + // Round to digits after the decimal point; exponential if number is large + ModeFixed + // Always exponential format; significant digits + ModeExponential + // Either fixed or exponential format; significant digits + ModePrecision +) + +func insert(b []byte, p int, c byte) []byte { + b = append(b, 0) + copy(b[p+1:], b[p:]) + b[p] = c + return b +} + +func expand(b []byte, delta int) []byte { + newLen := len(b) + delta + if newLen <= cap(b) { + return b[:newLen] + } + b1 := make([]byte, newLen) + copy(b1, b) + return b1 +} + +func FToStr(d float64, mode FToStrMode, precision int, buffer []byte) []byte { + if math.IsNaN(d) { + buffer = append(buffer, "NaN"...) + return buffer + } + if math.IsInf(d, 0) { + if math.Signbit(d) { + buffer = append(buffer, '-') + } + buffer = append(buffer, "Infinity"...) + return buffer + } + + if mode == ModeFixed && (d >= 1e21 || d <= -1e21) { + mode = ModeStandard + } + + var decPt int + var ok bool + startPos := len(buffer) + + if d != 0 { // also matches -0 + if d < 0 { + buffer = append(buffer, '-') + d = -d + startPos++ + } + switch mode { + case ModeStandard, ModeStandardExponential: + buffer, decPt, ok = fast.Dtoa(d, fast.ModeShortest, 0, buffer) + case ModeExponential, ModePrecision: + buffer, decPt, ok = fast.Dtoa(d, fast.ModePrecision, precision, buffer) + } + } else { + buffer = append(buffer, '0') + decPt, ok = 1, true + } + if !ok { + buffer, decPt = ftoa(d, dtoaModes[mode], mode >= ModeFixed, precision, buffer) + } + exponentialNotation := false + minNDigits := 0 /* Minimum number of significand digits required by mode and precision */ + nDigits := len(buffer) - startPos + + switch mode { + case ModeStandard: + if decPt < -5 || decPt > 21 { + exponentialNotation = true + } else { + minNDigits = decPt + } + case ModeFixed: + if precision >= 0 { + minNDigits = decPt + precision + } else { + minNDigits = decPt + } + case ModeExponential: + // JS_ASSERT(precision > 0); + minNDigits = precision + fallthrough + case ModeStandardExponential: + exponentialNotation = true + case ModePrecision: + // JS_ASSERT(precision > 0); + minNDigits = precision + if decPt < -5 || decPt > precision { + exponentialNotation = true + } + } + + for nDigits < minNDigits { + buffer = append(buffer, '0') + nDigits++ + } + + if exponentialNotation { + /* Insert a decimal point if more than one significand digit */ + if nDigits != 1 { + buffer = insert(buffer, startPos+1, '.') + } + buffer = append(buffer, 'e') + if decPt-1 >= 0 { + buffer = append(buffer, '+') + } + buffer = strconv.AppendInt(buffer, int64(decPt-1), 10) + } else if decPt != nDigits { + /* Some kind of a fraction in fixed notation */ + // JS_ASSERT(decPt <= nDigits); + if decPt > 0 { + /* dd...dd . dd...dd */ + buffer = insert(buffer, startPos+decPt, '.') + } else { + /* 0 . 00...00dd...dd */ + buffer = expand(buffer, 2-decPt) + copy(buffer[startPos+2-decPt:], buffer[startPos:]) + buffer[startPos] = '0' + buffer[startPos+1] = '.' + for i := startPos + 2; i < startPos+2-decPt; i++ { + buffer[i] = '0' + } + } + } + + return buffer +} diff --git a/goja/ftoa/ftostr_test.go b/goja/ftoa/ftostr_test.go new file mode 100644 index 0000000..db2441f --- /dev/null +++ b/goja/ftoa/ftostr_test.go @@ -0,0 +1,92 @@ +package ftoa + +import ( + "math" + "strconv" + "testing" +) + +func _testFToStr(num float64, mode FToStrMode, precision int, expected string, t *testing.T) { + buf := FToStr(num, mode, precision, nil) + if s := string(buf); s != expected { + t.Fatalf("expected: '%s', actual: '%s", expected, s) + } + if !math.IsNaN(num) && num != 0 && !math.Signbit(num) { + _testFToStr(-num, mode, precision, "-"+expected, t) + } +} + +func testFToStr(num float64, mode FToStrMode, precision int, expected string, t *testing.T) { + t.Run("", func(t *testing.T) { + t.Parallel() + _testFToStr(num, mode, precision, expected, t) + }) +} + +func TestDtostr(t *testing.T) { + testFToStr(0, ModeStandard, 0, "0", t) + testFToStr(1, ModeStandard, 0, "1", t) + testFToStr(9007199254740991, ModeStandard, 0, "9007199254740991", t) + testFToStr(math.MaxInt64, ModeStandardExponential, 0, "9.223372036854776e+18", t) + testFToStr(1e-5, ModeFixed, 1, "0.0", t) + testFToStr(8.85, ModeExponential, 2, "8.8e+0", t) + testFToStr(885, ModeExponential, 2, "8.9e+2", t) + testFToStr(25, ModeExponential, 1, "3e+1", t) + testFToStr(1e-6, ModeFixed, 7, "0.0000010", t) + testFToStr(math.Pi, ModeStandardExponential, 0, "3.141592653589793e+0", t) + testFToStr(math.Inf(1), ModeStandard, 0, "Infinity", t) + testFToStr(math.NaN(), ModeStandard, 0, "NaN", t) + testFToStr(math.SmallestNonzeroFloat64, ModeExponential, 40, "4.940656458412465441765687928682213723651e-324", t) + testFToStr(3.5844466002796428e+298, ModeStandard, 0, "3.5844466002796428e+298", t) + testFToStr(math.Float64frombits(0x0010000000000000), ModeStandard, 0, "2.2250738585072014e-308", t) // smallest normal + testFToStr(math.Float64frombits(0x000FFFFFFFFFFFFF), ModeStandard, 0, "2.225073858507201e-308", t) // largest denormal + testFToStr(4294967272.0, ModePrecision, 14, "4294967272.0000", t) +} + +func BenchmarkDtostrSmall(b *testing.B) { + var buf [128]byte + b.ReportAllocs() + for i := 0; i < b.N; i++ { + FToStr(math.Pi, ModeStandardExponential, 0, buf[:0]) + } +} + +func BenchmarkDtostrShort(b *testing.B) { + var buf [128]byte + b.ReportAllocs() + for i := 0; i < b.N; i++ { + FToStr(3.1415, ModeStandard, 0, buf[:0]) + } +} + +func BenchmarkDtostrFixed(b *testing.B) { + var buf [128]byte + b.ReportAllocs() + for i := 0; i < b.N; i++ { + FToStr(math.Pi, ModeFixed, 4, buf[:0]) + } +} + +func BenchmarkDtostrBig(b *testing.B) { + var buf [128]byte + b.ReportAllocs() + for i := 0; i < b.N; i++ { + FToStr(math.SmallestNonzeroFloat64, ModeExponential, 40, buf[:0]) + } +} + +func BenchmarkAppendFloatBig(b *testing.B) { + var buf [128]byte + b.ReportAllocs() + for i := 0; i < b.N; i++ { + strconv.AppendFloat(buf[:0], math.SmallestNonzeroFloat64, 'e', 40, 64) + } +} + +func BenchmarkAppendFloatSmall(b *testing.B) { + var buf [128]byte + b.ReportAllocs() + for i := 0; i < b.N; i++ { + strconv.AppendFloat(buf[:0], math.Pi, 'e', -1, 64) + } +} diff --git a/goja/ftoa/internal/fast/LICENSE_V8 b/goja/ftoa/internal/fast/LICENSE_V8 new file mode 100644 index 0000000..bbad266 --- /dev/null +++ b/goja/ftoa/internal/fast/LICENSE_V8 @@ -0,0 +1,26 @@ +Copyright 2014, the V8 project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/goja/ftoa/internal/fast/cachedpower.go b/goja/ftoa/internal/fast/cachedpower.go new file mode 100644 index 0000000..4f7e49f --- /dev/null +++ b/goja/ftoa/internal/fast/cachedpower.go @@ -0,0 +1,120 @@ +package fast + +import "math" + +const ( + kCachedPowersOffset = 348 // -1 * the first decimal_exponent. + kD_1_LOG2_10 = 0.30102999566398114 // 1 / lg(10) + kDecimalExponentDistance = 8 +) + +type cachedPower struct { + significand uint64 + binary_exponent int16 + decimal_exponent int16 +} + +var ( + cachedPowers = [...]cachedPower{ + {0xFA8FD5A0081C0288, -1220, -348}, + {0xBAAEE17FA23EBF76, -1193, -340}, + {0x8B16FB203055AC76, -1166, -332}, + {0xCF42894A5DCE35EA, -1140, -324}, + {0x9A6BB0AA55653B2D, -1113, -316}, + {0xE61ACF033D1A45DF, -1087, -308}, + {0xAB70FE17C79AC6CA, -1060, -300}, + {0xFF77B1FCBEBCDC4F, -1034, -292}, + {0xBE5691EF416BD60C, -1007, -284}, + {0x8DD01FAD907FFC3C, -980, -276}, + {0xD3515C2831559A83, -954, -268}, + {0x9D71AC8FADA6C9B5, -927, -260}, + {0xEA9C227723EE8BCB, -901, -252}, + {0xAECC49914078536D, -874, -244}, + {0x823C12795DB6CE57, -847, -236}, + {0xC21094364DFB5637, -821, -228}, + {0x9096EA6F3848984F, -794, -220}, + {0xD77485CB25823AC7, -768, -212}, + {0xA086CFCD97BF97F4, -741, -204}, + {0xEF340A98172AACE5, -715, -196}, + {0xB23867FB2A35B28E, -688, -188}, + {0x84C8D4DFD2C63F3B, -661, -180}, + {0xC5DD44271AD3CDBA, -635, -172}, + {0x936B9FCEBB25C996, -608, -164}, + {0xDBAC6C247D62A584, -582, -156}, + {0xA3AB66580D5FDAF6, -555, -148}, + {0xF3E2F893DEC3F126, -529, -140}, + {0xB5B5ADA8AAFF80B8, -502, -132}, + {0x87625F056C7C4A8B, -475, -124}, + {0xC9BCFF6034C13053, -449, -116}, + {0x964E858C91BA2655, -422, -108}, + {0xDFF9772470297EBD, -396, -100}, + {0xA6DFBD9FB8E5B88F, -369, -92}, + {0xF8A95FCF88747D94, -343, -84}, + {0xB94470938FA89BCF, -316, -76}, + {0x8A08F0F8BF0F156B, -289, -68}, + {0xCDB02555653131B6, -263, -60}, + {0x993FE2C6D07B7FAC, -236, -52}, + {0xE45C10C42A2B3B06, -210, -44}, + {0xAA242499697392D3, -183, -36}, + {0xFD87B5F28300CA0E, -157, -28}, + {0xBCE5086492111AEB, -130, -20}, + {0x8CBCCC096F5088CC, -103, -12}, + {0xD1B71758E219652C, -77, -4}, + {0x9C40000000000000, -50, 4}, + {0xE8D4A51000000000, -24, 12}, + {0xAD78EBC5AC620000, 3, 20}, + {0x813F3978F8940984, 30, 28}, + {0xC097CE7BC90715B3, 56, 36}, + {0x8F7E32CE7BEA5C70, 83, 44}, + {0xD5D238A4ABE98068, 109, 52}, + {0x9F4F2726179A2245, 136, 60}, + {0xED63A231D4C4FB27, 162, 68}, + {0xB0DE65388CC8ADA8, 189, 76}, + {0x83C7088E1AAB65DB, 216, 84}, + {0xC45D1DF942711D9A, 242, 92}, + {0x924D692CA61BE758, 269, 100}, + {0xDA01EE641A708DEA, 295, 108}, + {0xA26DA3999AEF774A, 322, 116}, + {0xF209787BB47D6B85, 348, 124}, + {0xB454E4A179DD1877, 375, 132}, + {0x865B86925B9BC5C2, 402, 140}, + {0xC83553C5C8965D3D, 428, 148}, + {0x952AB45CFA97A0B3, 455, 156}, + {0xDE469FBD99A05FE3, 481, 164}, + {0xA59BC234DB398C25, 508, 172}, + {0xF6C69A72A3989F5C, 534, 180}, + {0xB7DCBF5354E9BECE, 561, 188}, + {0x88FCF317F22241E2, 588, 196}, + {0xCC20CE9BD35C78A5, 614, 204}, + {0x98165AF37B2153DF, 641, 212}, + {0xE2A0B5DC971F303A, 667, 220}, + {0xA8D9D1535CE3B396, 694, 228}, + {0xFB9B7CD9A4A7443C, 720, 236}, + {0xBB764C4CA7A44410, 747, 244}, + {0x8BAB8EEFB6409C1A, 774, 252}, + {0xD01FEF10A657842C, 800, 260}, + {0x9B10A4E5E9913129, 827, 268}, + {0xE7109BFBA19C0C9D, 853, 276}, + {0xAC2820D9623BF429, 880, 284}, + {0x80444B5E7AA7CF85, 907, 292}, + {0xBF21E44003ACDD2D, 933, 300}, + {0x8E679C2F5E44FF8F, 960, 308}, + {0xD433179D9C8CB841, 986, 316}, + {0x9E19DB92B4E31BA9, 1013, 324}, + {0xEB96BF6EBADF77D9, 1039, 332}, + {0xAF87023B9BF0EE6B, 1066, 340}, + } +) + +func getCachedPowerForBinaryExponentRange(min_exponent, max_exponent int) (power diyfp, decimal_exponent int) { + kQ := diyFpKSignificandSize + k := int(math.Ceil(float64(min_exponent+kQ-1) * kD_1_LOG2_10)) + index := (kCachedPowersOffset+k-1)/kDecimalExponentDistance + 1 + cached_power := cachedPowers[index] + _DCHECK(min_exponent <= int(cached_power.binary_exponent)) + _DCHECK(int(cached_power.binary_exponent) <= max_exponent) + decimal_exponent = int(cached_power.decimal_exponent) + power = diyfp{f: cached_power.significand, e: int(cached_power.binary_exponent)} + + return +} diff --git a/goja/ftoa/internal/fast/common.go b/goja/ftoa/internal/fast/common.go new file mode 100644 index 0000000..6ffaaf9 --- /dev/null +++ b/goja/ftoa/internal/fast/common.go @@ -0,0 +1,18 @@ +/* +Package fast contains code ported from V8 (https://github.com/v8/v8/blob/master/src/numbers/fast-dtoa.cc) + +See LICENSE_V8 for the original copyright message and disclaimer. +*/ +package fast + +import "errors" + +var ( + dcheckFailure = errors.New("DCHECK assertion failed") +) + +func _DCHECK(f bool) { + if !f { + panic(dcheckFailure) + } +} diff --git a/goja/ftoa/internal/fast/diyfp.go b/goja/ftoa/internal/fast/diyfp.go new file mode 100644 index 0000000..727a747 --- /dev/null +++ b/goja/ftoa/internal/fast/diyfp.go @@ -0,0 +1,152 @@ +package fast + +import "math" + +const ( + diyFpKSignificandSize = 64 + kSignificandSize = 53 + kUint64MSB uint64 = 1 << 63 + + kSignificandMask = 0x000FFFFFFFFFFFFF + kHiddenBit = 0x0010000000000000 + kExponentMask = 0x7FF0000000000000 + + kPhysicalSignificandSize = 52 // Excludes the hidden bit. + kExponentBias = 0x3FF + kPhysicalSignificandSize + kDenormalExponent = -kExponentBias + 1 +) + +type double float64 + +type diyfp struct { + f uint64 + e int +} + +// f =- o. +// The exponents of both numbers must be the same and the significand of this +// must be bigger than the significand of other. +// The result will not be normalized. +func (f *diyfp) subtract(o diyfp) { + _DCHECK(f.e == o.e) + _DCHECK(f.f >= o.f) + f.f -= o.f +} + +// Returns f - o +// The exponents of both numbers must be the same and this must be bigger +// than other. The result will not be normalized. +func (f diyfp) minus(o diyfp) diyfp { + res := f + res.subtract(o) + return res +} + +// f *= o +func (f *diyfp) mul(o diyfp) { + // Simply "emulates" a 128 bit multiplication. + // However: the resulting number only contains 64 bits. The least + // significant 64 bits are only used for rounding the most significant 64 + // bits. + const kM32 uint64 = 0xFFFFFFFF + a := f.f >> 32 + b := f.f & kM32 + c := o.f >> 32 + d := o.f & kM32 + ac := a * c + bc := b * c + ad := a * d + bd := b * d + tmp := (bd >> 32) + (ad & kM32) + (bc & kM32) + // By adding 1U << 31 to tmp we round the final result. + // Halfway cases will be round up. + tmp += 1 << 31 + result_f := ac + (ad >> 32) + (bc >> 32) + (tmp >> 32) + f.e += o.e + 64 + f.f = result_f +} + +// Returns f * o +func (f diyfp) times(o diyfp) diyfp { + res := f + res.mul(o) + return res +} + +func (f *diyfp) _normalize() { + f_, e := f.f, f.e + // This method is mainly called for normalizing boundaries. In general + // boundaries need to be shifted by 10 bits. We thus optimize for this case. + const k10MSBits uint64 = 0x3FF << 54 + for f_&k10MSBits == 0 { + f_ <<= 10 + e -= 10 + } + for f_&kUint64MSB == 0 { + f_ <<= 1 + e-- + } + f.f, f.e = f_, e +} + +func normalizeDiyfp(f diyfp) diyfp { + res := f + res._normalize() + return res +} + +// f must be strictly greater than 0. +func (d double) toNormalizedDiyfp() diyfp { + f, e := d.sigExp() + + // The current float could be a denormal. + for (f & kHiddenBit) == 0 { + f <<= 1 + e-- + } + // Do the final shifts in one go. + f <<= diyFpKSignificandSize - kSignificandSize + e -= diyFpKSignificandSize - kSignificandSize + return diyfp{f, e} +} + +// Returns the two boundaries of this. +// The bigger boundary (m_plus) is normalized. The lower boundary has the same +// exponent as m_plus. +// Precondition: the value encoded by this Double must be greater than 0. +func (d double) normalizedBoundaries() (m_minus, m_plus diyfp) { + v := d.toDiyFp() + significand_is_zero := v.f == kHiddenBit + m_plus = normalizeDiyfp(diyfp{f: (v.f << 1) + 1, e: v.e - 1}) + if significand_is_zero && v.e != kDenormalExponent { + // The boundary is closer. Think of v = 1000e10 and v- = 9999e9. + // Then the boundary (== (v - v-)/2) is not just at a distance of 1e9 but + // at a distance of 1e8. + // The only exception is for the smallest normal: the largest denormal is + // at the same distance as its successor. + // Note: denormals have the same exponent as the smallest normals. + m_minus = diyfp{f: (v.f << 2) - 1, e: v.e - 2} + } else { + m_minus = diyfp{f: (v.f << 1) - 1, e: v.e - 1} + } + m_minus.f <<= m_minus.e - m_plus.e + m_minus.e = m_plus.e + return +} + +func (d double) toDiyFp() diyfp { + f, e := d.sigExp() + return diyfp{f: f, e: e} +} + +func (d double) sigExp() (significand uint64, exponent int) { + d64 := math.Float64bits(float64(d)) + significand = d64 & kSignificandMask + if d64&kExponentMask != 0 { // not denormal + significand += kHiddenBit + exponent = int((d64&kExponentMask)>>kPhysicalSignificandSize) - kExponentBias + } else { + exponent = kDenormalExponent + } + return +} diff --git a/goja/ftoa/internal/fast/dtoa.go b/goja/ftoa/internal/fast/dtoa.go new file mode 100644 index 0000000..b12870a --- /dev/null +++ b/goja/ftoa/internal/fast/dtoa.go @@ -0,0 +1,642 @@ +package fast + +import ( + "fmt" + "strconv" +) + +const ( + kMinimalTargetExponent = -60 + kMaximalTargetExponent = -32 + + kTen4 = 10000 + kTen5 = 100000 + kTen6 = 1000000 + kTen7 = 10000000 + kTen8 = 100000000 + kTen9 = 1000000000 +) + +type Mode int + +const ( + ModeShortest Mode = iota + ModePrecision +) + +// Adjusts the last digit of the generated number, and screens out generated +// solutions that may be inaccurate. A solution may be inaccurate if it is +// outside the safe interval, or if we cannot prove that it is closer to the +// input than a neighboring representation of the same length. +// +// Input: * buffer containing the digits of too_high / 10^kappa +// - distance_too_high_w == (too_high - w).f() * unit +// - unsafe_interval == (too_high - too_low).f() * unit +// - rest = (too_high - buffer * 10^kappa).f() * unit +// - ten_kappa = 10^kappa * unit +// - unit = the common multiplier +// +// Output: returns true if the buffer is guaranteed to contain the closest +// +// representable number to the input. +// Modifies the generated digits in the buffer to approach (round towards) w. +func roundWeed(buffer []byte, distance_too_high_w, unsafe_interval, rest, ten_kappa, unit uint64) bool { + small_distance := distance_too_high_w - unit + big_distance := distance_too_high_w + unit + + // Let w_low = too_high - big_distance, and + // w_high = too_high - small_distance. + // Note: w_low < w < w_high + // + // The real w (* unit) must lie somewhere inside the interval + // ]w_low; w_high[ (often written as "(w_low; w_high)") + + // Basically the buffer currently contains a number in the unsafe interval + // ]too_low; too_high[ with too_low < w < too_high + // + // too_high - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // ^v 1 unit ^ ^ ^ ^ + // boundary_high --------------------- . . . . + // ^v 1 unit . . . . + // - - - - - - - - - - - - - - - - - - - + - - + - - - - - - . . + // . . ^ . . + // . big_distance . . . + // . . . . rest + // small_distance . . . . + // v . . . . + // w_high - - - - - - - - - - - - - - - - - - . . . . + // ^v 1 unit . . . . + // w ---------------------------------------- . . . . + // ^v 1 unit v . . . + // w_low - - - - - - - - - - - - - - - - - - - - - . . . + // . . v + // buffer --------------------------------------------------+-------+-------- + // . . + // safe_interval . + // v . + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - . + // ^v 1 unit . + // boundary_low ------------------------- unsafe_interval + // ^v 1 unit v + // too_low - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // + // + // Note that the value of buffer could lie anywhere inside the range too_low + // to too_high. + // + // boundary_low, boundary_high and w are approximations of the real boundaries + // and v (the input number). They are guaranteed to be precise up to one unit. + // In fact the error is guaranteed to be strictly less than one unit. + // + // Anything that lies outside the unsafe interval is guaranteed not to round + // to v when read again. + // Anything that lies inside the safe interval is guaranteed to round to v + // when read again. + // If the number inside the buffer lies inside the unsafe interval but not + // inside the safe interval then we simply do not know and bail out (returning + // false). + // + // Similarly we have to take into account the imprecision of 'w' when finding + // the closest representation of 'w'. If we have two potential + // representations, and one is closer to both w_low and w_high, then we know + // it is closer to the actual value v. + // + // By generating the digits of too_high we got the largest (closest to + // too_high) buffer that is still in the unsafe interval. In the case where + // w_high < buffer < too_high we try to decrement the buffer. + // This way the buffer approaches (rounds towards) w. + // There are 3 conditions that stop the decrementation process: + // 1) the buffer is already below w_high + // 2) decrementing the buffer would make it leave the unsafe interval + // 3) decrementing the buffer would yield a number below w_high and farther + // away than the current number. In other words: + // (buffer{-1} < w_high) && w_high - buffer{-1} > buffer - w_high + // Instead of using the buffer directly we use its distance to too_high. + // Conceptually rest ~= too_high - buffer + // We need to do the following tests in this order to avoid over- and + // underflows. + _DCHECK(rest <= unsafe_interval) + for rest < small_distance && // Negated condition 1 + unsafe_interval-rest >= ten_kappa && // Negated condition 2 + (rest+ten_kappa < small_distance || // buffer{-1} > w_high + small_distance-rest >= rest+ten_kappa-small_distance) { + buffer[len(buffer)-1]-- + rest += ten_kappa + } + + // We have approached w+ as much as possible. We now test if approaching w- + // would require changing the buffer. If yes, then we have two possible + // representations close to w, but we cannot decide which one is closer. + if rest < big_distance && unsafe_interval-rest >= ten_kappa && + (rest+ten_kappa < big_distance || + big_distance-rest > rest+ten_kappa-big_distance) { + return false + } + + // Weeding test. + // The safe interval is [too_low + 2 ulp; too_high - 2 ulp] + // Since too_low = too_high - unsafe_interval this is equivalent to + // [too_high - unsafe_interval + 4 ulp; too_high - 2 ulp] + // Conceptually we have: rest ~= too_high - buffer + return (2*unit <= rest) && (rest <= unsafe_interval-4*unit) +} + +// Rounds the buffer upwards if the result is closer to v by possibly adding +// 1 to the buffer. If the precision of the calculation is not sufficient to +// round correctly, return false. +// The rounding might shift the whole buffer in which case the kappa is +// adjusted. For example "99", kappa = 3 might become "10", kappa = 4. +// +// If 2*rest > ten_kappa then the buffer needs to be round up. +// rest can have an error of +/- 1 unit. This function accounts for the +// imprecision and returns false, if the rounding direction cannot be +// unambiguously determined. +// +// Precondition: rest < ten_kappa. +func roundWeedCounted(buffer []byte, rest, ten_kappa, unit uint64, kappa *int) bool { + _DCHECK(rest < ten_kappa) + // The following tests are done in a specific order to avoid overflows. They + // will work correctly with any uint64 values of rest < ten_kappa and unit. + // + // If the unit is too big, then we don't know which way to round. For example + // a unit of 50 means that the real number lies within rest +/- 50. If + // 10^kappa == 40 then there is no way to tell which way to round. + if unit >= ten_kappa { + return false + } + // Even if unit is just half the size of 10^kappa we are already completely + // lost. (And after the previous test we know that the expression will not + // over/underflow.) + if ten_kappa-unit <= unit { + return false + } + // If 2 * (rest + unit) <= 10^kappa we can safely round down. + if (ten_kappa-rest > rest) && (ten_kappa-2*rest >= 2*unit) { + return true + } + + // If 2 * (rest - unit) >= 10^kappa, then we can safely round up. + if (rest > unit) && (ten_kappa-(rest-unit) <= (rest - unit)) { + // Increment the last digit recursively until we find a non '9' digit. + buffer[len(buffer)-1]++ + for i := len(buffer) - 1; i > 0; i-- { + if buffer[i] != '0'+10 { + break + } + buffer[i] = '0' + buffer[i-1]++ + } + // If the first digit is now '0'+ 10 we had a buffer with all '9's. With the + // exception of the first digit all digits are now '0'. Simply switch the + // first digit to '1' and adjust the kappa. Example: "99" becomes "10" and + // the power (the kappa) is increased. + if buffer[0] == '0'+10 { + buffer[0] = '1' + *kappa += 1 + } + return true + } + return false +} + +// Returns the biggest power of ten that is less than or equal than the given +// number. We furthermore receive the maximum number of bits 'number' has. +// If number_bits == 0 then 0^-1 is returned +// The number of bits must be <= 32. +// Precondition: number < (1 << (number_bits + 1)). +func biggestPowerTen(number uint32, number_bits int) (power uint32, exponent int) { + switch number_bits { + case 32, 31, 30: + if kTen9 <= number { + power = kTen9 + exponent = 9 + break + } + fallthrough + case 29, 28, 27: + if kTen8 <= number { + power = kTen8 + exponent = 8 + break + } + fallthrough + case 26, 25, 24: + if kTen7 <= number { + power = kTen7 + exponent = 7 + break + } + fallthrough + case 23, 22, 21, 20: + if kTen6 <= number { + power = kTen6 + exponent = 6 + break + } + fallthrough + case 19, 18, 17: + if kTen5 <= number { + power = kTen5 + exponent = 5 + break + } + fallthrough + case 16, 15, 14: + if kTen4 <= number { + power = kTen4 + exponent = 4 + break + } + fallthrough + case 13, 12, 11, 10: + if 1000 <= number { + power = 1000 + exponent = 3 + break + } + fallthrough + case 9, 8, 7: + if 100 <= number { + power = 100 + exponent = 2 + break + } + fallthrough + case 6, 5, 4: + if 10 <= number { + power = 10 + exponent = 1 + break + } + fallthrough + case 3, 2, 1: + if 1 <= number { + power = 1 + exponent = 0 + break + } + fallthrough + case 0: + power = 0 + exponent = -1 + } + return +} + +// Generates the digits of input number w. +// w is a floating-point number (DiyFp), consisting of a significand and an +// exponent. Its exponent is bounded by kMinimalTargetExponent and +// kMaximalTargetExponent. +// +// Hence -60 <= w.e() <= -32. +// +// Returns false if it fails, in which case the generated digits in the buffer +// should not be used. +// Preconditions: +// - low, w and high are correct up to 1 ulp (unit in the last place). That +// is, their error must be less than a unit of their last digits. +// - low.e() == w.e() == high.e() +// - low < w < high, and taking into account their error: low~ <= high~ +// - kMinimalTargetExponent <= w.e() <= kMaximalTargetExponent +// +// Postconditions: returns false if procedure fails. +// +// otherwise: +// * buffer is not null-terminated, but len contains the number of digits. +// * buffer contains the shortest possible decimal digit-sequence +// such that LOW < buffer * 10^kappa < HIGH, where LOW and HIGH are the +// correct values of low and high (without their error). +// * if more than one decimal representation gives the minimal number of +// decimal digits then the one closest to W (where W is the correct value +// of w) is chosen. +// +// Remark: this procedure takes into account the imprecision of its input +// +// numbers. If the precision is not enough to guarantee all the postconditions +// then false is returned. This usually happens rarely (~0.5%). +// +// Say, for the sake of example, that +// +// w.e() == -48, and w.f() == 0x1234567890ABCDEF +// +// w's value can be computed by w.f() * 2^w.e() +// We can obtain w's integral digits by simply shifting w.f() by -w.e(). +// +// -> w's integral part is 0x1234 +// w's fractional part is therefore 0x567890ABCDEF. +// +// Printing w's integral part is easy (simply print 0x1234 in decimal). +// In order to print its fraction we repeatedly multiply the fraction by 10 and +// get each digit. Example the first digit after the point would be computed by +// +// (0x567890ABCDEF * 10) >> 48. -> 3 +// +// The whole thing becomes slightly more complicated because we want to stop +// once we have enough digits. That is, once the digits inside the buffer +// represent 'w' we can stop. Everything inside the interval low - high +// represents w. However we have to pay attention to low, high and w's +// imprecision. +func digitGen(low, w, high diyfp, buffer []byte) (kappa int, buf []byte, res bool) { + _DCHECK(low.e == w.e && w.e == high.e) + _DCHECK(low.f+1 <= high.f-1) + _DCHECK(kMinimalTargetExponent <= w.e && w.e <= kMaximalTargetExponent) + // low, w and high are imprecise, but by less than one ulp (unit in the last + // place). + // If we remove (resp. add) 1 ulp from low (resp. high) we are certain that + // the new numbers are outside of the interval we want the final + // representation to lie in. + // Inversely adding (resp. removing) 1 ulp from low (resp. high) would yield + // numbers that are certain to lie in the interval. We will use this fact + // later on. + // We will now start by generating the digits within the uncertain + // interval. Later we will weed out representations that lie outside the safe + // interval and thus _might_ lie outside the correct interval. + unit := uint64(1) + too_low := diyfp{f: low.f - unit, e: low.e} + too_high := diyfp{f: high.f + unit, e: high.e} + // too_low and too_high are guaranteed to lie outside the interval we want the + // generated number in. + unsafe_interval := too_high.minus(too_low) + // We now cut the input number into two parts: the integral digits and the + // fractionals. We will not write any decimal separator though, but adapt + // kappa instead. + // Reminder: we are currently computing the digits (stored inside the buffer) + // such that: too_low < buffer * 10^kappa < too_high + // We use too_high for the digit_generation and stop as soon as possible. + // If we stop early we effectively round down. + one := diyfp{f: 1 << -w.e, e: w.e} + // Division by one is a shift. + integrals := uint32(too_high.f >> -one.e) + // Modulo by one is an and. + fractionals := too_high.f & (one.f - 1) + divisor, divisor_exponent := biggestPowerTen(integrals, diyFpKSignificandSize-(-one.e)) + kappa = divisor_exponent + 1 + buf = buffer + for kappa > 0 { + digit := int(integrals / divisor) + buf = append(buf, byte('0'+digit)) + integrals %= divisor + kappa-- + // Note that kappa now equals the exponent of the divisor and that the + // invariant thus holds again. + rest := uint64(integrals)<<-one.e + fractionals + // Invariant: too_high = buffer * 10^kappa + DiyFp(rest, one.e) + // Reminder: unsafe_interval.e == one.e + if rest < unsafe_interval.f { + // Rounding down (by not emitting the remaining digits) yields a number + // that lies within the unsafe interval. + res = roundWeed(buf, too_high.minus(w).f, + unsafe_interval.f, rest, + uint64(divisor)<<-one.e, unit) + return + } + divisor /= 10 + } + // The integrals have been generated. We are at the point of the decimal + // separator. In the following loop we simply multiply the remaining digits by + // 10 and divide by one. We just need to pay attention to multiply associated + // data (like the interval or 'unit'), too. + // Note that the multiplication by 10 does not overflow, because w.e >= -60 + // and thus one.e >= -60. + _DCHECK(one.e >= -60) + _DCHECK(fractionals < one.f) + _DCHECK(0xFFFFFFFFFFFFFFFF/10 >= one.f) + for { + fractionals *= 10 + unit *= 10 + unsafe_interval.f *= 10 + // Integer division by one. + digit := byte(fractionals >> -one.e) + buf = append(buf, '0'+digit) + fractionals &= one.f - 1 // Modulo by one. + kappa-- + if fractionals < unsafe_interval.f { + res = roundWeed(buf, too_high.minus(w).f*unit, unsafe_interval.f, fractionals, one.f, unit) + return + } + } +} + +// Generates (at most) requested_digits of input number w. +// w is a floating-point number (DiyFp), consisting of a significand and an +// exponent. Its exponent is bounded by kMinimalTargetExponent and +// kMaximalTargetExponent. +// +// Hence -60 <= w.e() <= -32. +// +// Returns false if it fails, in which case the generated digits in the buffer +// should not be used. +// Preconditions: +// - w is correct up to 1 ulp (unit in the last place). That +// is, its error must be strictly less than a unit of its last digit. +// - kMinimalTargetExponent <= w.e() <= kMaximalTargetExponent +// +// Postconditions: returns false if procedure fails. +// +// otherwise: +// * buffer is not null-terminated, but length contains the number of +// digits. +// * the representation in buffer is the most precise representation of +// requested_digits digits. +// * buffer contains at most requested_digits digits of w. If there are less +// than requested_digits digits then some trailing '0's have been removed. +// * kappa is such that +// w = buffer * 10^kappa + eps with |eps| < 10^kappa / 2. +// +// Remark: This procedure takes into account the imprecision of its input +// +// numbers. If the precision is not enough to guarantee all the postconditions +// then false is returned. This usually happens rarely, but the failure-rate +// increases with higher requested_digits. +func digitGenCounted(w diyfp, requested_digits int, buffer []byte) (kappa int, buf []byte, res bool) { + _DCHECK(kMinimalTargetExponent <= w.e && w.e <= kMaximalTargetExponent) + + // w is assumed to have an error less than 1 unit. Whenever w is scaled we + // also scale its error. + w_error := uint64(1) + // We cut the input number into two parts: the integral digits and the + // fractional digits. We don't emit any decimal separator, but adapt kappa + // instead. Example: instead of writing "1.2" we put "12" into the buffer and + // increase kappa by 1. + one := diyfp{f: 1 << -w.e, e: w.e} + // Division by one is a shift. + integrals := uint32(w.f >> -one.e) + // Modulo by one is an and. + fractionals := w.f & (one.f - 1) + divisor, divisor_exponent := biggestPowerTen(integrals, diyFpKSignificandSize-(-one.e)) + kappa = divisor_exponent + 1 + buf = buffer + // Loop invariant: buffer = w / 10^kappa (integer division) + // The invariant holds for the first iteration: kappa has been initialized + // with the divisor exponent + 1. And the divisor is the biggest power of ten + // that is smaller than 'integrals'. + for kappa > 0 { + digit := byte(integrals / divisor) + buf = append(buf, '0'+digit) + requested_digits-- + integrals %= divisor + kappa-- + // Note that kappa now equals the exponent of the divisor and that the + // invariant thus holds again. + if requested_digits == 0 { + break + } + divisor /= 10 + } + + if requested_digits == 0 { + rest := uint64(integrals)<<-one.e + fractionals + res = roundWeedCounted(buf, rest, uint64(divisor)<<-one.e, w_error, &kappa) + return + } + + // The integrals have been generated. We are at the point of the decimal + // separator. In the following loop we simply multiply the remaining digits by + // 10 and divide by one. We just need to pay attention to multiply associated + // data (the 'unit'), too. + // Note that the multiplication by 10 does not overflow, because w.e >= -60 + // and thus one.e >= -60. + _DCHECK(one.e >= -60) + _DCHECK(fractionals < one.f) + _DCHECK(0xFFFFFFFFFFFFFFFF/10 >= one.f) + for requested_digits > 0 && fractionals > w_error { + fractionals *= 10 + w_error *= 10 + // Integer division by one. + digit := byte(fractionals >> -one.e) + buf = append(buf, '0'+digit) + requested_digits-- + fractionals &= one.f - 1 // Modulo by one. + kappa-- + } + if requested_digits != 0 { + res = false + } else { + res = roundWeedCounted(buf, fractionals, one.f, w_error, &kappa) + } + return +} + +// Provides a decimal representation of v. +// Returns true if it succeeds, otherwise the result cannot be trusted. +// There will be *length digits inside the buffer (not null-terminated). +// If the function returns true then +// +// v == (double) (buffer * 10^decimal_exponent). +// +// The digits in the buffer are the shortest representation possible: no +// 0.09999999999999999 instead of 0.1. The shorter representation will even be +// chosen even if the longer one would be closer to v. +// The last digit will be closest to the actual v. That is, even if several +// digits might correctly yield 'v' when read again, the closest will be +// computed. +func grisu3(f float64, buffer []byte) (digits []byte, decimal_exponent int, result bool) { + v := double(f) + w := v.toNormalizedDiyfp() + + // boundary_minus and boundary_plus are the boundaries between v and its + // closest floating-point neighbors. Any number strictly between + // boundary_minus and boundary_plus will round to v when convert to a double. + // Grisu3 will never output representations that lie exactly on a boundary. + boundary_minus, boundary_plus := v.normalizedBoundaries() + ten_mk_minimal_binary_exponent := kMinimalTargetExponent - (w.e + diyFpKSignificandSize) + ten_mk_maximal_binary_exponent := kMaximalTargetExponent - (w.e + diyFpKSignificandSize) + ten_mk, mk := getCachedPowerForBinaryExponentRange(ten_mk_minimal_binary_exponent, ten_mk_maximal_binary_exponent) + + _DCHECK( + (kMinimalTargetExponent <= + w.e+ten_mk.e+diyFpKSignificandSize) && + (kMaximalTargetExponent >= w.e+ten_mk.e+diyFpKSignificandSize)) + // Note that ten_mk is only an approximation of 10^-k. A DiyFp only contains a + // 64 bit significand and ten_mk is thus only precise up to 64 bits. + + // The DiyFp::Times procedure rounds its result, and ten_mk is approximated + // too. The variable scaled_w (as well as scaled_boundary_minus/plus) are now + // off by a small amount. + // In fact: scaled_w - w*10^k < 1ulp (unit in the last place) of scaled_w. + // In other words: let f = scaled_w.f() and e = scaled_w.e(), then + // (f-1) * 2^e < w*10^k < (f+1) * 2^e + scaled_w := w.times(ten_mk) + _DCHECK(scaled_w.e == + boundary_plus.e+ten_mk.e+diyFpKSignificandSize) + // In theory it would be possible to avoid some recomputations by computing + // the difference between w and boundary_minus/plus (a power of 2) and to + // compute scaled_boundary_minus/plus by subtracting/adding from + // scaled_w. However the code becomes much less readable and the speed + // enhancements are not terrific. + scaled_boundary_minus := boundary_minus.times(ten_mk) + scaled_boundary_plus := boundary_plus.times(ten_mk) + // DigitGen will generate the digits of scaled_w. Therefore we have + // v == (double) (scaled_w * 10^-mk). + // Set decimal_exponent == -mk and pass it to DigitGen. If scaled_w is not an + // integer than it will be updated. For instance if scaled_w == 1.23 then + // the buffer will be filled with "123" und the decimal_exponent will be + // decreased by 2. + var kappa int + kappa, digits, result = digitGen(scaled_boundary_minus, scaled_w, scaled_boundary_plus, buffer) + decimal_exponent = -mk + kappa + return +} + +// The "counted" version of grisu3 (see above) only generates requested_digits +// number of digits. This version does not generate the shortest representation, +// and with enough requested digits 0.1 will at some point print as 0.9999999... +// Grisu3 is too imprecise for real halfway cases (1.5 will not work) and +// therefore the rounding strategy for halfway cases is irrelevant. +func grisu3Counted(v float64, requested_digits int, buffer []byte) (digits []byte, decimal_exponent int, result bool) { + w := double(v).toNormalizedDiyfp() + ten_mk_minimal_binary_exponent := kMinimalTargetExponent - (w.e + diyFpKSignificandSize) + ten_mk_maximal_binary_exponent := kMaximalTargetExponent - (w.e + diyFpKSignificandSize) + ten_mk, mk := getCachedPowerForBinaryExponentRange(ten_mk_minimal_binary_exponent, ten_mk_maximal_binary_exponent) + + _DCHECK( + (kMinimalTargetExponent <= + w.e+ten_mk.e+diyFpKSignificandSize) && + (kMaximalTargetExponent >= w.e+ten_mk.e+diyFpKSignificandSize)) + // Note that ten_mk is only an approximation of 10^-k. A DiyFp only contains a + // 64 bit significand and ten_mk is thus only precise up to 64 bits. + + // The DiyFp::Times procedure rounds its result, and ten_mk is approximated + // too. The variable scaled_w (as well as scaled_boundary_minus/plus) are now + // off by a small amount. + // In fact: scaled_w - w*10^k < 1ulp (unit in the last place) of scaled_w. + // In other words: let f = scaled_w.f() and e = scaled_w.e(), then + // (f-1) * 2^e < w*10^k < (f+1) * 2^e + scaled_w := w.times(ten_mk) + // We now have (double) (scaled_w * 10^-mk). + // DigitGen will generate the first requested_digits digits of scaled_w and + // return together with a kappa such that scaled_w ~= buffer * 10^kappa. (It + // will not always be exactly the same since DigitGenCounted only produces a + // limited number of digits.) + var kappa int + kappa, digits, result = digitGenCounted(scaled_w, requested_digits, buffer) + decimal_exponent = -mk + kappa + + return +} + +// v must be > 0 and must not be Inf or NaN +func Dtoa(v float64, mode Mode, requested_digits int, buffer []byte) (digits []byte, decimal_point int, result bool) { + defer func() { + if x := recover(); x != nil { + if x == dcheckFailure { + panic(fmt.Errorf("DCHECK assertion failed while formatting %s in mode %d", strconv.FormatFloat(v, 'e', 50, 64), mode)) + } + panic(x) + } + }() + var decimal_exponent int + startPos := len(buffer) + switch mode { + case ModeShortest: + digits, decimal_exponent, result = grisu3(v, buffer) + case ModePrecision: + digits, decimal_exponent, result = grisu3Counted(v, requested_digits, buffer) + } + if result { + decimal_point = len(digits) - startPos + decimal_exponent + } else { + digits = digits[:startPos] + } + return +} diff --git a/goja/func.go b/goja/func.go new file mode 100644 index 0000000..047df1c --- /dev/null +++ b/goja/func.go @@ -0,0 +1,1116 @@ +package goja + +import ( + "fmt" + "reflect" + "strings" + + "github.com/dop251/goja/unistring" +) + +type resultType uint8 + +const ( + resultNormal resultType = iota + resultYield + resultYieldRes // a yield that expects a value in return + resultYieldDelegate // yield* + resultYieldDelegateRes + resultAwait +) + +// used both as an instruction and as a Value +type yieldMarker struct { + valueNull + resultType resultType +} + +var ( + await = &yieldMarker{resultType: resultAwait} + + yield = &yieldMarker{resultType: resultYield} + yieldRes = &yieldMarker{resultType: resultYieldRes} + yieldDelegate = &yieldMarker{resultType: resultYieldDelegate} + yieldDelegateRes = &yieldMarker{resultType: resultYieldDelegateRes} + yieldEmpty = &yieldMarker{resultType: resultYield} +) + +// AsyncContextTracker is a handler that allows to track an async execution context to ensure it remains +// consistent across all callback invocations. +// Whenever a Promise reaction job is scheduled the Grab method is called. It is supposed to return the +// current context. The same context will be supplied to the Resumed method before the reaction job is +// executed. The Exited method is called after the reaction job is finished. +// This means that for each invocation of the Grab method there will be exactly one subsequent invocation +// of Resumed and then Exited methods (assuming the Promise is fulfilled or rejected). Also, the Resumed/Exited +// calls cannot be nested, so Exited can simply clear the current context instead of popping from a stack. +// Note, this works for both async functions and regular Promise.then()/Promise.catch() callbacks. +// See TestAsyncContextTracker for more insight. +// +// To register it call Runtime.SetAsyncContextTracker(). +type AsyncContextTracker interface { + Grab() (trackingObject interface{}) + Resumed(trackingObject interface{}) + Exited() +} + +type funcObjectImpl interface { + source() String +} + +type baseFuncObject struct { + baseObject + + lenProp valueProperty +} + +type baseJsFuncObject struct { + baseFuncObject + + stash *stash + privEnv *privateEnv + + prg *Program + src string + strict bool +} + +type funcObject struct { + baseJsFuncObject +} + +type generatorFuncObject struct { + baseJsFuncObject +} + +type asyncFuncObject struct { + baseJsFuncObject +} + +type classFuncObject struct { + baseJsFuncObject + initFields *Program + computedKeys []Value + + privateEnvType *privateEnvType + privateMethods []Value + + derived bool +} + +type methodFuncObject struct { + baseJsFuncObject + homeObject *Object +} + +type generatorMethodFuncObject struct { + methodFuncObject +} + +type asyncMethodFuncObject struct { + methodFuncObject +} + +type arrowFuncObject struct { + baseJsFuncObject + funcObj *Object + newTarget Value +} + +type asyncArrowFuncObject struct { + arrowFuncObject +} + +type nativeFuncObject struct { + baseFuncObject + + f func(FunctionCall) Value + construct func(args []Value, newTarget *Object) *Object +} + +type wrappedFuncObject struct { + nativeFuncObject + wrapped reflect.Value +} + +type boundFuncObject struct { + nativeFuncObject + wrapped *Object +} + +type generatorState uint8 + +const ( + genStateUndefined generatorState = iota + genStateSuspendedStart + genStateExecuting + genStateSuspendedYield + genStateSuspendedYieldRes + genStateCompleted +) + +type generatorObject struct { + baseObject + gen generator + delegated *iteratorRecord + state generatorState +} + +func (f *nativeFuncObject) source() String { + return newStringValue(fmt.Sprintf("function %s() { [native code] }", nilSafe(f.getStr("name", nil)).toString())) +} + +func (f *nativeFuncObject) export(*objectExportCtx) interface{} { + return f.f +} + +func (f *wrappedFuncObject) exportType() reflect.Type { + return f.wrapped.Type() +} + +func (f *wrappedFuncObject) export(*objectExportCtx) interface{} { + return f.wrapped.Interface() +} + +func (f *funcObject) _addProto(n unistring.String) Value { + if n == "prototype" { + if _, exists := f.values[n]; !exists { + return f.addPrototype() + } + } + return nil +} + +func (f *funcObject) getStr(p unistring.String, receiver Value) Value { + return f.getStrWithOwnProp(f.getOwnPropStr(p), p, receiver) +} + +func (f *funcObject) getOwnPropStr(name unistring.String) Value { + if v := f._addProto(name); v != nil { + return v + } + + return f.baseObject.getOwnPropStr(name) +} + +func (f *funcObject) setOwnStr(name unistring.String, val Value, throw bool) bool { + f._addProto(name) + return f.baseObject.setOwnStr(name, val, throw) +} + +func (f *funcObject) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { + return f._setForeignStr(name, f.getOwnPropStr(name), val, receiver, throw) +} + +func (f *funcObject) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + f._addProto(name) + return f.baseObject.defineOwnPropertyStr(name, descr, throw) +} + +func (f *funcObject) deleteStr(name unistring.String, throw bool) bool { + f._addProto(name) + return f.baseObject.deleteStr(name, throw) +} + +func (f *funcObject) addPrototype() Value { + proto := f.val.runtime.NewObject() + proto.self._putProp("constructor", f.val, true, false, true) + return f._putProp("prototype", proto, true, false, false) +} + +func (f *funcObject) hasOwnPropertyStr(name unistring.String) bool { + if f.baseObject.hasOwnPropertyStr(name) { + return true + } + + if name == "prototype" { + return true + } + return false +} + +func (f *funcObject) stringKeys(all bool, accum []Value) []Value { + if all { + if _, exists := f.values["prototype"]; !exists { + accum = append(accum, asciiString("prototype")) + } + } + return f.baseFuncObject.stringKeys(all, accum) +} + +func (f *funcObject) iterateStringKeys() iterNextFunc { + if _, exists := f.values["prototype"]; !exists { + f.addPrototype() + } + return f.baseFuncObject.iterateStringKeys() +} + +func (f *baseFuncObject) createInstance(newTarget *Object) *Object { + r := f.val.runtime + if newTarget == nil { + newTarget = f.val + } + proto := r.getPrototypeFromCtor(newTarget, nil, r.global.ObjectPrototype) + + return f.val.runtime.newBaseObject(proto, classObject).val +} + +func (f *baseJsFuncObject) source() String { + return newStringValue(f.src) +} + +func (f *baseJsFuncObject) construct(args []Value, newTarget *Object) *Object { + if newTarget == nil { + newTarget = f.val + } + proto := newTarget.self.getStr("prototype", nil) + var protoObj *Object + if p, ok := proto.(*Object); ok { + protoObj = p + } else { + protoObj = f.val.runtime.global.ObjectPrototype + } + + obj := f.val.runtime.newBaseObject(protoObj, classObject).val + ret := f.call(FunctionCall{ + This: obj, + Arguments: args, + }, newTarget) + + if ret, ok := ret.(*Object); ok { + return ret + } + return obj +} + +func (f *classFuncObject) Call(FunctionCall) Value { + panic(f.val.runtime.NewTypeError("Class constructor cannot be invoked without 'new'")) +} + +func (f *classFuncObject) assertCallable() (func(FunctionCall) Value, bool) { + return f.Call, true +} + +func (f *classFuncObject) vmCall(vm *vm, n int) { + f.Call(FunctionCall{}) +} + +func (f *classFuncObject) export(*objectExportCtx) interface{} { + return f.Call +} + +func (f *classFuncObject) createInstance(args []Value, newTarget *Object) (instance *Object) { + if f.derived { + if ctor := f.prototype.self.assertConstructor(); ctor != nil { + instance = ctor(args, newTarget) + } else { + panic(f.val.runtime.NewTypeError("Super constructor is not a constructor")) + } + } else { + instance = f.baseFuncObject.createInstance(newTarget) + } + return +} + +func (f *classFuncObject) _initFields(instance *Object) { + if f.privateEnvType != nil { + penv := instance.self.getPrivateEnv(f.privateEnvType, true) + penv.methods = f.privateMethods + } + if f.initFields != nil { + vm := f.val.runtime.vm + vm.pushCtx() + vm.prg = f.initFields + vm.stash = f.stash + vm.privEnv = f.privEnv + vm.newTarget = nil + + // so that 'super' base could be correctly resolved (including from direct eval()) + vm.push(f.val) + + vm.sb = vm.sp + vm.push(instance) + vm.pc = 0 + ex := vm.runTry() + vm.popCtx() + if ex != nil { + panic(ex) + } + vm.sp -= 2 + } +} + +func (f *classFuncObject) construct(args []Value, newTarget *Object) *Object { + if newTarget == nil { + newTarget = f.val + } + if f.prg == nil { + instance := f.createInstance(args, newTarget) + f._initFields(instance) + return instance + } else { + var instance *Object + var thisVal Value + if !f.derived { + instance = f.createInstance(args, newTarget) + f._initFields(instance) + thisVal = instance + } + ret := f._call(args, newTarget, thisVal) + + if ret, ok := ret.(*Object); ok { + return ret + } + if f.derived { + r := f.val.runtime + if ret != _undefined { + panic(r.NewTypeError("Derived constructors may only return object or undefined")) + } + if v := r.vm.stack[r.vm.sp+1]; v != nil { // using residual 'this' value (a bit hacky) + instance = r.toObject(v) + } else { + panic(r.newError(r.getReferenceError(), "Must call super constructor in derived class before returning from derived constructor")) + } + } + return instance + } +} + +func (f *classFuncObject) assertConstructor() func(args []Value, newTarget *Object) *Object { + return f.construct +} + +func (f *baseJsFuncObject) Call(call FunctionCall) Value { + return f.call(call, nil) +} + +func (f *arrowFuncObject) Call(call FunctionCall) Value { + return f._call(call.Arguments, f.newTarget, nil) +} + +func (f *baseJsFuncObject) __call(args []Value, newTarget, this Value) (Value, *Exception) { + vm := f.val.runtime.vm + + vm.stack.expand(vm.sp + len(args) + 1) + vm.stack[vm.sp] = f.val + vm.sp++ + vm.stack[vm.sp] = this + vm.sp++ + for _, arg := range args { + if arg != nil { + vm.stack[vm.sp] = arg + } else { + vm.stack[vm.sp] = _undefined + } + vm.sp++ + } + + vm.pushTryFrame(tryPanicMarker, -1) + defer vm.popTryFrame() + + var needPop bool + if vm.prg != nil { + vm.pushCtx() + vm.callStack = append(vm.callStack, context{pc: -2}) // extra frame so that run() halts after ret + needPop = true + } else { + vm.pc = -2 + vm.pushCtx() + } + + vm.args = len(args) + vm.prg = f.prg + vm.stash = f.stash + vm.privEnv = f.privEnv + vm.newTarget = newTarget + vm.pc = 0 + for { + ex := vm.runTryInner() + if ex != nil { + return nil, ex + } + if vm.halted() { + break + } + } + if needPop { + vm.popCtx() + } + + return vm.pop(), nil +} + +func (f *baseJsFuncObject) _call(args []Value, newTarget, this Value) Value { + res, ex := f.__call(args, newTarget, this) + if ex != nil { + panic(ex) + } + return res +} + +func (f *baseJsFuncObject) call(call FunctionCall, newTarget Value) Value { + return f._call(call.Arguments, newTarget, nilSafe(call.This)) +} + +func (f *baseJsFuncObject) export(*objectExportCtx) interface{} { + return f.Call +} + +func (f *baseFuncObject) exportType() reflect.Type { + return reflectTypeFunc +} + +func (f *baseFuncObject) typeOf() String { + return stringFunction +} + +func (f *baseJsFuncObject) assertCallable() (func(FunctionCall) Value, bool) { + return f.Call, true +} + +func (f *funcObject) assertConstructor() func(args []Value, newTarget *Object) *Object { + return f.construct +} + +func (f *baseJsFuncObject) vmCall(vm *vm, n int) { + vm.pushCtx() + vm.args = n + vm.prg = f.prg + vm.stash = f.stash + vm.privEnv = f.privEnv + vm.pc = 0 + vm.stack[vm.sp-n-1], vm.stack[vm.sp-n-2] = vm.stack[vm.sp-n-2], vm.stack[vm.sp-n-1] +} + +func (f *arrowFuncObject) assertCallable() (func(FunctionCall) Value, bool) { + return f.Call, true +} + +func (f *arrowFuncObject) vmCall(vm *vm, n int) { + vm.pushCtx() + vm.args = n + vm.prg = f.prg + vm.stash = f.stash + vm.privEnv = f.privEnv + vm.pc = 0 + vm.stack[vm.sp-n-1], vm.stack[vm.sp-n-2] = nil, vm.stack[vm.sp-n-1] + vm.newTarget = f.newTarget +} + +func (f *arrowFuncObject) export(*objectExportCtx) interface{} { + return f.Call +} + +func (f *baseFuncObject) init(name unistring.String, length Value) { + f.baseObject.init() + + f.lenProp.configurable = true + f.lenProp.value = length + f._put("length", &f.lenProp) + + f._putProp("name", stringValueFromRaw(name), false, false, true) +} + +func hasInstance(val *Object, v Value) bool { + if v, ok := v.(*Object); ok { + o := val.self.getStr("prototype", nil) + if o1, ok := o.(*Object); ok { + for { + v = v.self.proto() + if v == nil { + return false + } + if o1 == v { + return true + } + } + } else { + panic(val.runtime.NewTypeError("prototype is not an object")) + } + } + + return false +} + +func (f *baseFuncObject) hasInstance(v Value) bool { + return hasInstance(f.val, v) +} + +func (f *nativeFuncObject) defaultConstruct(ccall func(ConstructorCall) *Object, args []Value, newTarget *Object) *Object { + obj := f.createInstance(newTarget) + ret := ccall(ConstructorCall{ + This: obj, + Arguments: args, + NewTarget: newTarget, + }) + + if ret != nil { + return ret + } + return obj +} + +func (f *nativeFuncObject) assertCallable() (func(FunctionCall) Value, bool) { + if f.f != nil { + return f.f, true + } + return nil, false +} + +func (f *nativeFuncObject) vmCall(vm *vm, n int) { + if f.f != nil { + vm.pushCtx() + vm.prg = nil + vm.sb = vm.sp - n // so that [sb-1] points to the callee + //fmt.Println(">>>>>>>>>>>>>>>>>", n, vm.stack[vm.sp-n:vm.sp]) + ret := f.f(FunctionCall{ + Arguments: vm.stack[vm.sp-n : vm.sp], + This: vm.stack[vm.sp-n-2], + }) + if ret == nil { + ret = _undefined + } + //fmt.Println(">>>>>>>>>>>>>>>>>>>>>>", ret.toString().String()) + if strings.HasPrefix(ret.toString().String(), "GoError: ") { + //fmt.Println(">>>>>>>>>>>>>>>>>>>>>>", 111) + //vm.throw(Exception{ + // val: ret.toString(), + //}) + vm.throw(ret) + } else { + vm.stack[vm.sp-n-2] = ret + vm.popCtx() + } + } else { + vm.stack[vm.sp-n-2] = _undefined + } + vm.sp -= n + 1 + vm.pc++ +} + +func (f *nativeFuncObject) assertConstructor() func(args []Value, newTarget *Object) *Object { + return f.construct +} + +func (f *boundFuncObject) hasInstance(v Value) bool { + return instanceOfOperator(v, f.wrapped) +} + +func (f *baseJsFuncObject) prepareForVmCall(call FunctionCall) { + vm := f.val.runtime.vm + args := call.Arguments + vm.stack.expand(vm.sp + len(args) + 1) + vm.stack[vm.sp] = call.This + vm.sp++ + vm.stack[vm.sp] = f.val + vm.sp++ + for _, arg := range args { + if arg != nil { + vm.stack[vm.sp] = arg + } else { + vm.stack[vm.sp] = _undefined + } + vm.sp++ + } +} + +func (f *baseJsFuncObject) asyncCall(call FunctionCall, vmCall func(*vm, int)) Value { + f.prepareForVmCall(call) + ar := &asyncRunner{ + f: f.val, + vmCall: vmCall, + } + ar.start(len(call.Arguments)) + return ar.promiseCap.promise +} + +func (f *asyncFuncObject) Call(call FunctionCall) Value { + return f.asyncCall(call, f.baseJsFuncObject.vmCall) +} + +func (f *asyncFuncObject) assertCallable() (func(FunctionCall) Value, bool) { + return f.Call, true +} + +func (f *asyncFuncObject) export(*objectExportCtx) interface{} { + return f.Call +} + +func (f *asyncArrowFuncObject) Call(call FunctionCall) Value { + return f.asyncCall(call, f.arrowFuncObject.vmCall) +} + +func (f *asyncArrowFuncObject) assertCallable() (func(FunctionCall) Value, bool) { + return f.Call, true +} + +func (f *asyncArrowFuncObject) export(*objectExportCtx) interface{} { + return f.Call +} + +func (f *asyncArrowFuncObject) vmCall(vm *vm, n int) { + f.asyncVmCall(vm, n, f.arrowFuncObject.vmCall) +} + +func (f *asyncMethodFuncObject) Call(call FunctionCall) Value { + return f.asyncCall(call, f.methodFuncObject.vmCall) +} + +func (f *asyncMethodFuncObject) assertCallable() (func(FunctionCall) Value, bool) { + return f.Call, true +} + +func (f *asyncMethodFuncObject) export(ctx *objectExportCtx) interface{} { + return f.Call +} + +func (f *asyncMethodFuncObject) vmCall(vm *vm, n int) { + f.asyncVmCall(vm, n, f.methodFuncObject.vmCall) +} + +func (f *baseJsFuncObject) asyncVmCall(vm *vm, n int, vmCall func(*vm, int)) { + ar := &asyncRunner{ + f: f.val, + vmCall: vmCall, + } + ar.start(n) + vm.push(ar.promiseCap.promise) + vm.pc++ +} + +func (f *asyncFuncObject) vmCall(vm *vm, n int) { + f.asyncVmCall(vm, n, f.baseJsFuncObject.vmCall) +} + +type asyncRunner struct { + gen generator + promiseCap *promiseCapability + f *Object + vmCall func(*vm, int) +} + +func (ar *asyncRunner) onFulfilled(call FunctionCall) Value { + ar.gen.vm.curAsyncRunner = ar + defer func() { + ar.gen.vm.curAsyncRunner = nil + }() + arg := call.Argument(0) + res, resType, ex := ar.gen.next(arg) + ar.step(res, resType == resultNormal, ex) + return _undefined +} + +func (ar *asyncRunner) onRejected(call FunctionCall) Value { + ar.gen.vm.curAsyncRunner = ar + defer func() { + ar.gen.vm.curAsyncRunner = nil + }() + reason := call.Argument(0) + res, resType, ex := ar.gen.nextThrow(reason) + ar.step(res, resType == resultNormal, ex) + return _undefined +} + +func (ar *asyncRunner) step(res Value, done bool, ex *Exception) { + r := ar.f.runtime + if done || ex != nil { + if ex == nil { + ar.promiseCap.resolve(res) + } else { + ar.promiseCap.reject(ex.val) + } + return + } + + // await + promise := r.promiseResolve(r.getPromise(), res) + promise.self.(*Promise).addReactions(&promiseReaction{ + typ: promiseReactionFulfill, + handler: &jobCallback{callback: ar.onFulfilled}, + asyncRunner: ar, + }, &promiseReaction{ + typ: promiseReactionReject, + handler: &jobCallback{callback: ar.onRejected}, + asyncRunner: ar, + }) +} + +func (ar *asyncRunner) start(nArgs int) { + r := ar.f.runtime + ar.gen.vm = r.vm + ar.promiseCap = r.newPromiseCapability(r.getPromise()) + sp := r.vm.sp + ar.gen.enter() + ar.vmCall(r.vm, nArgs) + res, resType, ex := ar.gen.step() + ar.step(res, resType == resultNormal, ex) + if ex != nil { + r.vm.sp = sp - nArgs - 2 + } + r.vm.popTryFrame() + r.vm.popCtx() +} + +type generator struct { + ctx execCtx + vm *vm + + tryStackLen, iterStackLen, refStackLen uint32 +} + +func (g *generator) storeLengths() { + g.tryStackLen, g.iterStackLen, g.refStackLen = uint32(len(g.vm.tryStack)), uint32(len(g.vm.iterStack)), uint32(len(g.vm.refStack)) +} + +func (g *generator) enter() { + g.vm.pushCtx() + g.vm.pushTryFrame(tryPanicMarker, -1) + g.vm.prg, g.vm.sb, g.vm.pc = nil, -1, -2 // so that vm.run() halts after ret + g.storeLengths() +} + +func (g *generator) step() (res Value, resultType resultType, ex *Exception) { + for { + ex = g.vm.runTryInner() + if ex != nil { + return + } + if g.vm.halted() { + break + } + } + res = g.vm.pop() + if ym, ok := res.(*yieldMarker); ok { + resultType = ym.resultType + g.ctx = execCtx{} + g.vm.pc = -g.vm.pc + 1 + if res != yieldEmpty { + res = g.vm.pop() + } else { + res = nil + } + g.vm.suspend(&g.ctx, g.tryStackLen, g.iterStackLen, g.refStackLen) + g.vm.sp = g.vm.sb - 1 + g.vm.callStack = g.vm.callStack[:len(g.vm.callStack)-1] // remove the frame with pc == -2, as ret would do + } + return +} + +func (g *generator) enterNext() { + g.vm.pushCtx() + g.vm.pushTryFrame(tryPanicMarker, -1) + g.vm.callStack = append(g.vm.callStack, context{pc: -2}) // extra frame so that vm.run() halts after ret + g.storeLengths() + g.vm.resume(&g.ctx) +} + +func (g *generator) next(v Value) (Value, resultType, *Exception) { + g.enterNext() + if v != nil { + g.vm.push(v) + } + res, done, ex := g.step() + g.vm.popTryFrame() + g.vm.popCtx() + return res, done, ex +} + +func (g *generator) nextThrow(v interface{}) (Value, resultType, *Exception) { + g.enterNext() + ex := g.vm.handleThrow(v) + if ex != nil { + g.vm.popTryFrame() + g.vm.popCtx() + return nil, resultNormal, ex + } + + res, resType, ex := g.step() + g.vm.popTryFrame() + g.vm.popCtx() + return res, resType, ex +} + +func (g *generatorObject) init(vmCall func(*vm, int), nArgs int) { + g.baseObject.init() + vm := g.val.runtime.vm + g.gen.vm = vm + + g.gen.enter() + vmCall(vm, nArgs) + + _, _, ex := g.gen.step() + + vm.popTryFrame() + if ex != nil { + panic(ex) + } + + g.state = genStateSuspendedStart + vm.popCtx() +} + +func (g *generatorObject) validate() { + if g.state == genStateExecuting { + panic(g.val.runtime.NewTypeError("Illegal generator state")) + } +} + +func (g *generatorObject) step(res Value, resType resultType, ex *Exception) Value { + if ex != nil { + g.delegated = nil + g.state = genStateCompleted + panic(ex) + } + switch resType { + case resultYield: + g.state = genStateSuspendedYield + return g.val.runtime.createIterResultObject(res, false) + case resultYieldDelegate: + g.state = genStateSuspendedYield + return g.delegate(res) + case resultYieldRes: + g.state = genStateSuspendedYieldRes + return g.val.runtime.createIterResultObject(res, false) + case resultYieldDelegateRes: + g.state = genStateSuspendedYieldRes + return g.delegate(res) + case resultNormal: + g.state = genStateCompleted + return g.val.runtime.createIterResultObject(res, true) + default: + panic(g.val.runtime.NewTypeError("Runtime bug: unexpected result type: %v", resType)) + } +} + +func (g *generatorObject) delegate(v Value) Value { + ex := g.val.runtime.try(func() { + g.delegated = g.val.runtime.getIterator(v, nil) + }) + if ex != nil { + g.delegated = nil + g.state = genStateCompleted + return g.step(g.gen.nextThrow(ex)) + } + return g.next(_undefined) +} + +func (g *generatorObject) tryCallDelegated(fn func() (Value, bool)) (ret Value, done bool) { + ex := g.val.runtime.try(func() { + ret, done = fn() + }) + if ex != nil { + g.delegated = nil + g.state = genStateExecuting + return g.step(g.gen.nextThrow(ex)), false + } + return +} + +func (g *generatorObject) callDelegated(method func(FunctionCall) Value, v Value) (Value, bool) { + res := g.val.runtime.toObject(method(FunctionCall{This: g.delegated.iterator, Arguments: []Value{v}})) + if iteratorComplete(res) { + g.delegated = nil + return iteratorValue(res), true + } + return res, false +} + +func (g *generatorObject) next(v Value) Value { + g.validate() + if g.state == genStateCompleted { + return g.val.runtime.createIterResultObject(_undefined, true) + } + if g.delegated != nil { + res, done := g.tryCallDelegated(func() (Value, bool) { + return g.callDelegated(g.delegated.next, v) + }) + if !done { + return res + } else { + v = res + } + } + if g.state != genStateSuspendedYieldRes { + v = nil + } + g.state = genStateExecuting + return g.step(g.gen.next(v)) +} + +func (g *generatorObject) throw(v Value) Value { + g.validate() + if g.state == genStateSuspendedStart { + g.state = genStateCompleted + } + if g.state == genStateCompleted { + panic(v) + } + if d := g.delegated; d != nil { + res, done := g.tryCallDelegated(func() (Value, bool) { + method := toMethod(g.delegated.iterator.self.getStr("throw", nil)) + if method != nil { + return g.callDelegated(method, v) + } + g.delegated = nil + d.returnIter() + panic(g.val.runtime.NewTypeError("The iterator does not provide a 'throw' method")) + }) + if !done { + return res + } + if g.state != genStateSuspendedYieldRes { + res = nil + } + g.state = genStateExecuting + return g.step(g.gen.next(res)) + } + g.state = genStateExecuting + return g.step(g.gen.nextThrow(v)) +} + +func (g *generatorObject) _return(v Value) Value { + g.validate() + if g.state == genStateSuspendedStart { + g.state = genStateCompleted + } + + if g.state == genStateCompleted { + return g.val.runtime.createIterResultObject(v, true) + } + + if d := g.delegated; d != nil { + res, done := g.tryCallDelegated(func() (Value, bool) { + method := toMethod(g.delegated.iterator.self.getStr("return", nil)) + if method != nil { + return g.callDelegated(method, v) + } + g.delegated = nil + return v, true + }) + if !done { + return res + } else { + v = res + } + } + + g.state = genStateExecuting + + g.gen.enterNext() + + vm := g.gen.vm + var ex *Exception + for len(vm.tryStack) > 0 { + tf := &vm.tryStack[len(vm.tryStack)-1] + if int(tf.callStackLen) != len(vm.callStack) { + break + } + + if tf.finallyPos >= 0 { + vm.sp = int(tf.sp) + vm.stash = tf.stash + vm.privEnv = tf.privEnv + ex1 := vm.restoreStacks(tf.iterLen, tf.refLen) + if ex1 != nil { + ex = ex1 + vm.popTryFrame() + continue + } + + vm.pc = int(tf.finallyPos) + tf.catchPos = tryPanicMarker + tf.finallyPos = -1 + tf.finallyRet = -2 // -1 would cause it to continue after leaveFinally + for { + ex1 := vm.runTryInner() + if ex1 != nil { + ex = ex1 + vm.popTryFrame() + break + } + if vm.halted() { + break + } + } + } else { + vm.popTryFrame() + } + } + + g.state = genStateCompleted + + vm.popTryFrame() + + if ex == nil { + ex = vm.restoreStacks(g.gen.iterStackLen, g.gen.refStackLen) + } + + if ex != nil { + panic(ex) + } + + vm.callStack = vm.callStack[:len(vm.callStack)-1] + vm.sp = vm.sb - 1 + vm.popCtx() + + return g.val.runtime.createIterResultObject(v, true) +} + +func (f *baseJsFuncObject) generatorCall(vmCall func(*vm, int), nArgs int) Value { + o := &Object{runtime: f.val.runtime} + + genObj := &generatorObject{ + baseObject: baseObject{ + class: classObject, + val: o, + extensible: true, + }, + } + o.self = genObj + genObj.init(vmCall, nArgs) + genObj.prototype = o.runtime.getPrototypeFromCtor(f.val, nil, o.runtime.getGeneratorPrototype()) + return o +} + +func (f *baseJsFuncObject) generatorVmCall(vmCall func(*vm, int), nArgs int) { + vm := f.val.runtime.vm + vm.push(f.generatorCall(vmCall, nArgs)) + vm.pc++ +} + +func (f *generatorFuncObject) vmCall(_ *vm, nArgs int) { + f.generatorVmCall(f.baseJsFuncObject.vmCall, nArgs) +} + +func (f *generatorFuncObject) Call(call FunctionCall) Value { + f.prepareForVmCall(call) + return f.generatorCall(f.baseJsFuncObject.vmCall, len(call.Arguments)) +} + +func (f *generatorFuncObject) assertCallable() (func(FunctionCall) Value, bool) { + return f.Call, true +} + +func (f *generatorFuncObject) export(*objectExportCtx) interface{} { + return f.Call +} + +func (f *generatorFuncObject) assertConstructor() func(args []Value, newTarget *Object) *Object { + return nil +} + +func (f *generatorMethodFuncObject) vmCall(_ *vm, nArgs int) { + f.generatorVmCall(f.methodFuncObject.vmCall, nArgs) +} + +func (f *generatorMethodFuncObject) Call(call FunctionCall) Value { + f.prepareForVmCall(call) + return f.generatorCall(f.methodFuncObject.vmCall, len(call.Arguments)) +} + +func (f *generatorMethodFuncObject) assertCallable() (func(FunctionCall) Value, bool) { + return f.Call, true +} + +func (f *generatorMethodFuncObject) export(*objectExportCtx) interface{} { + return f.Call +} diff --git a/goja/func_test.go b/goja/func_test.go new file mode 100644 index 0000000..e3c8305 --- /dev/null +++ b/goja/func_test.go @@ -0,0 +1,309 @@ +package goja + +import ( + "errors" + "fmt" + "reflect" + "testing" +) + +func TestFuncProto(t *testing.T) { + const SCRIPT = ` + "use strict"; + function A() {} + A.__proto__ = Object; + A.prototype = {}; + + function B() {} + B.__proto__ = Object.create(null); + var thrown = false; + try { + delete B.prototype; + } catch (e) { + thrown = e instanceof TypeError; + } + thrown; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestFuncPrototypeRedefine(t *testing.T) { + const SCRIPT = ` + let thrown = false; + try { + Object.defineProperty(function() {}, "prototype", { + set: function(_value) {}, + }); + } catch (e) { + if (e instanceof TypeError) { + thrown = true; + } else { + throw e; + } + } + thrown; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestFuncExport(t *testing.T) { + vm := New() + typ := reflect.TypeOf((func(FunctionCall) Value)(nil)) + + f := func(expr string, t *testing.T) { + v, err := vm.RunString(expr) + if err != nil { + t.Fatal(err) + } + if actualTyp := v.ExportType(); actualTyp != typ { + t.Fatalf("Invalid export type: %v", actualTyp) + } + ev := v.Export() + if actualTyp := reflect.TypeOf(ev); actualTyp != typ { + t.Fatalf("Invalid export value: %v", ev) + } + } + + t.Run("regular function", func(t *testing.T) { + f("(function() {})", t) + }) + + t.Run("arrow function", func(t *testing.T) { + f("(()=>{})", t) + }) + + t.Run("method", func(t *testing.T) { + f("({m() {}}).m", t) + }) + + t.Run("class", func(t *testing.T) { + f("(class {})", t) + }) +} + +func TestFuncWrapUnwrap(t *testing.T) { + vm := New() + f := func(a int, b string) bool { + return a > 0 && b != "" + } + var f1 func(int, string) bool + v := vm.ToValue(f) + if et := v.ExportType(); et != reflect.TypeOf(f1) { + t.Fatal(et) + } + err := vm.ExportTo(v, &f1) + if err != nil { + t.Fatal(err) + } + if !f1(1, "a") { + t.Fatal("not true") + } +} + +func TestWrappedFunc(t *testing.T) { + vm := New() + f := func(a int, b string) bool { + return a > 0 && b != "" + } + vm.Set("f", f) + const SCRIPT = ` + assert.sameValue(typeof f, "function"); + const s = f.toString() + assert(s.endsWith("TestWrappedFunc.func1() { [native code] }"), s); + assert(f(1, "a")); + assert(!f(0, "")); + ` + vm.testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestWrappedFuncErrorPassthrough(t *testing.T) { + vm := New() + e := errors.New("test") + f := func(a int) error { + if a > 0 { + return e + } + return nil + } + + var f1 func(a int64) error + err := vm.ExportTo(vm.ToValue(f), &f1) + if err != nil { + t.Fatal(err) + } + if err := f1(1); err != e { + t.Fatal(err) + } +} + +func ExampleAssertConstructor() { + vm := New() + res, err := vm.RunString(` + (class C { + constructor(x) { + this.x = x; + } + }) + `) + if err != nil { + panic(err) + } + if ctor, ok := AssertConstructor(res); ok { + obj, err := ctor(nil, vm.ToValue("Test")) + if err != nil { + panic(err) + } + fmt.Print(obj.Get("x")) + } else { + panic("Not a constructor") + } + // Output: Test +} + +type testAsyncCtx struct { + group string + refCount int +} + +type testAsyncContextTracker struct { + ctx *testAsyncCtx + logFunc func(...interface{}) + resumed bool +} + +func (s *testAsyncContextTracker) Grab() interface{} { + ctx := s.ctx + if ctx != nil { + s.logFunc("Grab", ctx.group) + ctx.refCount++ + } + return ctx +} + +func (s *testAsyncContextTracker) Resumed(trackingObj interface{}) { + s.logFunc("Resumed", trackingObj) + if s.resumed { + panic("Nested Resumed() calls") + } + s.ctx = trackingObj.(*testAsyncCtx) + s.resumed = true +} + +func (s *testAsyncContextTracker) releaseCtx() { + s.ctx.refCount-- + if s.ctx.refCount < 0 { + panic("refCount < 0") + } + if s.ctx.refCount == 0 { + s.logFunc(s.ctx.group, "is finished") + } +} + +func (s *testAsyncContextTracker) Exited() { + s.logFunc("Exited") + if s.ctx != nil { + s.releaseCtx() + s.ctx = nil + } + s.resumed = false +} + +func TestAsyncContextTracker(t *testing.T) { + r := New() + var tracker testAsyncContextTracker + tracker.logFunc = t.Log + + group := func(name string, asyncFunc func(FunctionCall) Value) Value { + prevCtx := tracker.ctx + defer func() { + t.Log("Returned", name) + tracker.releaseCtx() + tracker.ctx = prevCtx + }() + tracker.ctx = &testAsyncCtx{ + group: name, + refCount: 1, + } + t.Log("Set", name) + return asyncFunc(FunctionCall{}) + } + r.SetAsyncContextTracker(&tracker) + r.Set("group", group) + r.Set("check", func(expectedGroup, msg string) { + var groupName string + if tracker.ctx != nil { + groupName = tracker.ctx.group + } + if groupName != expectedGroup { + t.Fatalf("Unexpected group (%q), expected %q in %s", groupName, expectedGroup, msg) + } + t.Log("In", msg) + }) + + t.Run("", func(t *testing.T) { + _, err := r.RunString(` + group("1", async () => { + check("1", "line A"); + await 3; + check("1", "line B"); + group("2", async () => { + check("2", "line C"); + await 4; + check("2", "line D"); + }) + }).then(() => { + check("", "line E"); + }) + `) + if err != nil { + t.Fatal(err) + } + }) + + t.Run("", func(t *testing.T) { + _, err := r.RunString(` + group("some", async () => { + check("some", "line A"); + (async () => { + check("some", "line B"); + await 1; + check("some", "line C"); + await 2; + check("some", "line D"); + })(); + check("some", "line E"); + }); + `) + if err != nil { + t.Fatal(err) + } + }) + + t.Run("", func(t *testing.T) { + _, err := r.RunString(` + group("Main", async () => { + check("Main", "0.1"); + await Promise.all([ + group("A", async () => { + check("A", "1.1"); + await 1; + check("A", "1.2"); + }), + (async () => { + check("Main", "3.1"); + })(), + group("B", async () => { + check("B", "2.1"); + await 2; + check("B", "2.2"); + }) + ]); + check("Main", "0.2"); + }); + `) + if err != nil { + t.Fatal(err) + } + }) +} diff --git a/goja/go.mod b/goja/go.mod new file mode 100644 index 0000000..103a3f1 --- /dev/null +++ b/goja/go.mod @@ -0,0 +1,15 @@ +module github.com/dop251/goja + +go 1.20 + +require ( + github.com/Masterminds/semver/v3 v3.2.1 + github.com/dlclark/regexp2 v1.11.4 + github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d + github.com/go-sourcemap/sourcemap v2.1.3+incompatible + github.com/google/pprof v0.0.0-20230207041349-798e818bf904 + golang.org/x/text v0.3.8 + gopkg.in/yaml.v2 v2.4.0 +) + +require github.com/kr/pretty v0.3.0 // indirect diff --git a/goja/goja/main.go b/goja/goja/main.go new file mode 100644 index 0000000..1015f46 --- /dev/null +++ b/goja/goja/main.go @@ -0,0 +1,127 @@ +package main + +import ( + crand "crypto/rand" + "encoding/binary" + "flag" + "fmt" + "io" + "log" + "math/rand" + "os" + "runtime/debug" + "runtime/pprof" + "time" + + "github.com/dop251/goja" + "github.com/dop251/goja_nodejs/console" + "github.com/dop251/goja_nodejs/require" +) + +var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file") +var timelimit = flag.Int("timelimit", 0, "max time to run (in seconds)") + +func readSource(filename string) ([]byte, error) { + if filename == "" || filename == "-" { + return io.ReadAll(os.Stdin) + } + return os.ReadFile(filename) +} + +func load(vm *goja.Runtime, call goja.FunctionCall) goja.Value { + p := call.Argument(0).String() + b, err := readSource(p) + if err != nil { + panic(vm.ToValue(fmt.Sprintf("Could not read %s: %v", p, err))) + } + v, err := vm.RunScript(p, string(b)) + if err != nil { + panic(err) + } + return v +} + +func newRandSource() goja.RandSource { + var seed int64 + if err := binary.Read(crand.Reader, binary.LittleEndian, &seed); err != nil { + panic(fmt.Errorf("Could not read random bytes: %v", err)) + } + return rand.New(rand.NewSource(seed)).Float64 +} + +func run() error { + filename := flag.Arg(0) + src, err := readSource(filename) + if err != nil { + return err + } + + if filename == "" || filename == "-" { + filename = "" + } + + vm := goja.New() + vm.SetRandSource(newRandSource()) + + new(require.Registry).Enable(vm) + console.Enable(vm) + + vm.Set("load", func(call goja.FunctionCall) goja.Value { + return load(vm, call) + }) + + vm.Set("readFile", func(name string) (string, error) { + b, err := os.ReadFile(name) + if err != nil { + return "", err + } + return string(b), nil + }) + + if *timelimit > 0 { + time.AfterFunc(time.Duration(*timelimit)*time.Second, func() { + vm.Interrupt("timeout") + }) + } + + //log.Println("Compiling...") + prg, err := goja.Compile(filename, string(src), false) + if err != nil { + return err + } + //log.Println("Running...") + _, err = vm.RunProgram(prg) + //log.Println("Finished.") + return err +} + +func main() { + defer func() { + if x := recover(); x != nil { + debug.Stack() + panic(x) + } + }() + flag.Parse() + if *cpuprofile != "" { + f, err := os.Create(*cpuprofile) + if err != nil { + log.Fatal(err) + } + pprof.StartCPUProfile(f) + defer pprof.StopCPUProfile() + } + + if err := run(); err != nil { + //fmt.Printf("err type: %T\n", err) + switch err := err.(type) { + case *goja.Exception: + fmt.Println(err.String()) + case *goja.InterruptedError: + fmt.Println(err.String()) + default: + fmt.Println(err) + } + os.Exit(64) + } +} diff --git a/goja/ipow.go b/goja/ipow.go new file mode 100644 index 0000000..5ee0d4d --- /dev/null +++ b/goja/ipow.go @@ -0,0 +1,98 @@ +package goja + +// inspired by https://gist.github.com/orlp/3551590 + +var overflows = [64]int64{ + 9223372036854775807, 9223372036854775807, 3037000499, 2097151, + 55108, 6208, 1448, 511, + 234, 127, 78, 52, + 38, 28, 22, 18, + 15, 13, 11, 9, + 8, 7, 7, 6, + 6, 5, 5, 5, + 4, 4, 4, 4, + 3, 3, 3, 3, + 3, 3, 3, 3, + 2, 2, 2, 2, + 2, 2, 2, 2, + 2, 2, 2, 2, + 2, 2, 2, 2, + 2, 2, 2, 2, + 2, 2, 2, 2, +} + +var highestBitSet = [63]byte{ + 0, 1, 2, 2, 3, 3, 3, 3, + 4, 4, 4, 4, 4, 4, 4, 4, + 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, + 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, +} + +func ipow(base, exp int64) (result int64) { + if exp >= 63 { + if base == 1 { + return 1 + } + + if base == -1 { + return 1 - 2*(exp&1) + } + + return 0 + } + + if base > overflows[exp] || -base > overflows[exp] { + return 0 + } + + result = 1 + + switch highestBitSet[byte(exp)] { + case 6: + if exp&1 != 0 { + result *= base + } + exp >>= 1 + base *= base + fallthrough + case 5: + if exp&1 != 0 { + result *= base + } + exp >>= 1 + base *= base + fallthrough + case 4: + if exp&1 != 0 { + result *= base + } + exp >>= 1 + base *= base + fallthrough + case 3: + if exp&1 != 0 { + result *= base + } + exp >>= 1 + base *= base + fallthrough + case 2: + if exp&1 != 0 { + result *= base + } + exp >>= 1 + base *= base + fallthrough + case 1: + if exp&1 != 0 { + result *= base + } + fallthrough + default: + return result + } +} diff --git a/goja/map.go b/goja/map.go new file mode 100644 index 0000000..b092b0d --- /dev/null +++ b/goja/map.go @@ -0,0 +1,169 @@ +package goja + +import ( + "hash/maphash" +) + +type mapEntry struct { + key, value Value + + iterPrev, iterNext *mapEntry + hNext *mapEntry +} + +type orderedMap struct { + hash *maphash.Hash + hashTable map[uint64]*mapEntry + iterFirst, iterLast *mapEntry + size int +} + +type orderedMapIter struct { + m *orderedMap + cur *mapEntry +} + +func (m *orderedMap) lookup(key Value) (h uint64, entry, hPrev *mapEntry) { + if key == _negativeZero { + key = intToValue(0) + } + h = key.hash(m.hash) + for entry = m.hashTable[h]; entry != nil && !entry.key.SameAs(key); hPrev, entry = entry, entry.hNext { + } + return +} + +func (m *orderedMap) set(key, value Value) { + h, entry, hPrev := m.lookup(key) + if entry != nil { + entry.value = value + } else { + if key == _negativeZero { + key = intToValue(0) + } + entry = &mapEntry{key: key, value: value} + if hPrev == nil { + m.hashTable[h] = entry + } else { + hPrev.hNext = entry + } + if m.iterLast != nil { + entry.iterPrev = m.iterLast + m.iterLast.iterNext = entry + } else { + m.iterFirst = entry + } + m.iterLast = entry + m.size++ + } +} + +func (m *orderedMap) get(key Value) Value { + _, entry, _ := m.lookup(key) + if entry != nil { + return entry.value + } + + return nil +} + +func (m *orderedMap) remove(key Value) bool { + h, entry, hPrev := m.lookup(key) + if entry != nil { + entry.key = nil + entry.value = nil + + // remove from the doubly-linked list + if entry.iterPrev != nil { + entry.iterPrev.iterNext = entry.iterNext + } else { + m.iterFirst = entry.iterNext + } + if entry.iterNext != nil { + entry.iterNext.iterPrev = entry.iterPrev + } else { + m.iterLast = entry.iterPrev + } + + // remove from the hashTable + if hPrev == nil { + if entry.hNext == nil { + delete(m.hashTable, h) + } else { + m.hashTable[h] = entry.hNext + } + } else { + hPrev.hNext = entry.hNext + } + + m.size-- + return true + } + + return false +} + +func (m *orderedMap) has(key Value) bool { + _, entry, _ := m.lookup(key) + return entry != nil +} + +func (iter *orderedMapIter) next() *mapEntry { + if iter.m == nil { + // closed iterator + return nil + } + + cur := iter.cur + // if the current item was deleted, track back to find the latest that wasn't + for cur != nil && cur.key == nil { + cur = cur.iterPrev + } + + if cur != nil { + cur = cur.iterNext + } else { + cur = iter.m.iterFirst + } + + if cur == nil { + iter.close() + } else { + iter.cur = cur + } + + return cur +} + +func (iter *orderedMapIter) close() { + iter.m = nil + iter.cur = nil +} + +func newOrderedMap(h *maphash.Hash) *orderedMap { + return &orderedMap{ + hash: h, + hashTable: make(map[uint64]*mapEntry), + } +} + +func (m *orderedMap) newIter() *orderedMapIter { + iter := &orderedMapIter{ + m: m, + } + return iter +} + +func (m *orderedMap) clear() { + for item := m.iterFirst; item != nil; item = item.iterNext { + item.key = nil + item.value = nil + if item.iterPrev != nil { + item.iterPrev.iterNext = nil + } + } + m.iterFirst = nil + m.iterLast = nil + m.hashTable = make(map[uint64]*mapEntry) + m.size = 0 +} diff --git a/goja/map_test.go b/goja/map_test.go new file mode 100644 index 0000000..8a743e4 --- /dev/null +++ b/goja/map_test.go @@ -0,0 +1,201 @@ +package goja + +import ( + "hash/maphash" + "math" + "math/big" + "strconv" + "testing" +) + +func testMapHashVal(v1, v2 Value, expected bool, t *testing.T) { + var h maphash.Hash + actual := v1.hash(&h) == v2.hash(&h) + if actual != expected { + t.Fatalf("testMapHashVal failed for %v, %v", v1, v2) + } +} + +func TestMapHash(t *testing.T) { + testMapHashVal(_NaN, _NaN, true, t) + testMapHashVal(valueTrue, valueFalse, false, t) + testMapHashVal(valueTrue, valueTrue, true, t) + testMapHashVal(intToValue(0), _negativeZero, true, t) + testMapHashVal(asciiString("Test"), asciiString("Test"), true, t) + testMapHashVal(newStringValue("Тест"), newStringValue("Тест"), true, t) + testMapHashVal(floatToValue(1.2345), floatToValue(1.2345), true, t) + testMapHashVal(SymIterator, SymToStringTag, false, t) + testMapHashVal(SymIterator, SymIterator, true, t) + testMapHashVal((*valueBigInt)(big.NewInt(1)), (*valueBigInt)(big.NewInt(-1)), false, t) + testMapHashVal((*valueBigInt)(big.NewInt(1)), (*valueBigInt)(big.NewInt(1)), true, t) + + // The following tests introduce indeterministic behaviour + //testMapHashVal(asciiString("Test"), asciiString("Test1"), false, t) + //testMapHashVal(newStringValue("Тест"), asciiString("Test"), false, t) + //testMapHashVal(newStringValue("Тест"), newStringValue("Тест1"), false, t) +} + +func TestOrderedMap(t *testing.T) { + m := newOrderedMap(&maphash.Hash{}) + for i := int64(0); i < 50; i++ { + m.set(intToValue(i), asciiString(strconv.FormatInt(i, 10))) + } + if m.size != 50 { + t.Fatalf("Unexpected size: %d", m.size) + } + + for i := int64(0); i < 50; i++ { + expected := asciiString(strconv.FormatInt(i, 10)) + actual := m.get(intToValue(i)) + if !expected.SameAs(actual) { + t.Fatalf("Wrong value for %d", i) + } + } + + for i := int64(0); i < 50; i += 2 { + if !m.remove(intToValue(i)) { + t.Fatalf("remove(%d) return false", i) + } + } + if m.size != 25 { + t.Fatalf("Unexpected size: %d", m.size) + } + + iter := m.newIter() + count := 0 + for { + entry := iter.next() + if entry == nil { + break + } + m.remove(entry.key) + count++ + } + + if count != 25 { + t.Fatalf("Unexpected iter count: %d", count) + } + + if m.size != 0 { + t.Fatalf("Unexpected size: %d", m.size) + } +} + +func TestOrderedMapCollision(t *testing.T) { + m := newOrderedMap(&maphash.Hash{}) + n1 := uint64(123456789) + n2 := math.Float64frombits(n1) + n1Key := intToValue(int64(n1)) + n2Key := floatToValue(n2) + m.set(n1Key, asciiString("n1")) + m.set(n2Key, asciiString("n2")) + if m.size == len(m.hashTable) { + t.Fatal("Expected a collision but there wasn't one") + } + if n2Val := m.get(n2Key); !asciiString("n2").SameAs(n2Val) { + t.Fatalf("unexpected n2Val: %v", n2Val) + } + if n1Val := m.get(n1Key); !asciiString("n1").SameAs(n1Val) { + t.Fatalf("unexpected nVal: %v", n1Val) + } + + if !m.remove(n1Key) { + t.Fatal("removing n1Key returned false") + } + if n2Val := m.get(n2Key); !asciiString("n2").SameAs(n2Val) { + t.Fatalf("2: unexpected n2Val: %v", n2Val) + } +} + +func TestOrderedMapIter(t *testing.T) { + m := newOrderedMap(&maphash.Hash{}) + iter := m.newIter() + ent := iter.next() + if ent != nil { + t.Fatal("entry should be nil") + } + iter1 := m.newIter() + m.set(intToValue(1), valueTrue) + ent = iter.next() + if ent != nil { + t.Fatal("2: entry should be nil") + } + ent = iter1.next() + if ent == nil { + t.Fatal("entry is nil") + } + if !intToValue(1).SameAs(ent.key) { + t.Fatal("unexpected key") + } + if !valueTrue.SameAs(ent.value) { + t.Fatal("unexpected value") + } +} + +func TestOrderedMapIterVisitAfterReAdd(t *testing.T) { + m := newOrderedMap(&maphash.Hash{}) + one := intToValue(1) + two := intToValue(2) + + m.set(one, valueTrue) + m.set(two, valueTrue) + iter := m.newIter() + entry := iter.next() + if !one.SameAs(entry.key) { + t.Fatalf("1: unexpected key: %v", entry.key) + } + if !m.remove(one) { + t.Fatal("remove returned false") + } + entry = iter.next() + if !two.SameAs(entry.key) { + t.Fatalf("2: unexpected key: %v", entry.key) + } + m.set(one, valueTrue) + entry = iter.next() + if entry == nil { + t.Fatal("entry is nil") + } + if !one.SameAs(entry.key) { + t.Fatalf("3: unexpected key: %v", entry.key) + } +} + +func TestOrderedMapIterAddAfterClear(t *testing.T) { + m := newOrderedMap(&maphash.Hash{}) + one := intToValue(1) + m.set(one, valueTrue) + iter := m.newIter() + iter.next() + m.clear() + m.set(one, valueTrue) + entry := iter.next() + if entry == nil { + t.Fatal("entry is nil") + } + if entry.key != one { + t.Fatalf("unexpected key: %v", entry.key) + } + entry = iter.next() + if entry != nil { + t.Fatalf("entry is not nil: %v", entry) + } +} + +func TestOrderedMapIterDeleteCurrent(t *testing.T) { + m := newOrderedMap(&maphash.Hash{}) + one := intToValue(1) + two := intToValue(2) + iter := m.newIter() + m.set(one, valueTrue) + m.set(two, valueTrue) + entry := iter.next() + if entry.key != one { + t.Fatalf("unexpected key: %v", entry.key) + } + m.remove(one) + entry = iter.next() + if entry.key != two { + t.Fatalf("2: unexpected key: %v", entry.key) + } +} diff --git a/goja/object.go b/goja/object.go new file mode 100644 index 0000000..79bd67d --- /dev/null +++ b/goja/object.go @@ -0,0 +1,1824 @@ +package goja + +import ( + "fmt" + "math" + "reflect" + "sort" + + "github.com/dop251/goja/unistring" +) + +const ( + classObject = "Object" + classArray = "Array" + classWeakSet = "WeakSet" + classWeakMap = "WeakMap" + classMap = "Map" + classMath = "Math" + classSet = "Set" + classFunction = "Function" + classAsyncFunction = "AsyncFunction" + classNumber = "Number" + classString = "String" + classBoolean = "Boolean" + classError = "Error" + classRegExp = "RegExp" + classDate = "Date" + classJSON = "JSON" + classGlobal = "global" + classPromise = "Promise" + + classArrayIterator = "Array Iterator" + classMapIterator = "Map Iterator" + classSetIterator = "Set Iterator" + classStringIterator = "String Iterator" + classRegExpStringIterator = "RegExp String Iterator" + + classGenerator = "Generator" + classGeneratorFunction = "GeneratorFunction" +) + +var ( + hintDefault Value = asciiString("default") + hintNumber Value = asciiString("number") + hintString Value = asciiString("string") +) + +type Object struct { + id uint64 + runtime *Runtime + self objectImpl + + weakRefs map[weakMap]Value +} + +type iterNextFunc func() (propIterItem, iterNextFunc) + +type PropertyDescriptor struct { + jsDescriptor *Object + + Value Value + + Writable, Configurable, Enumerable Flag + + Getter, Setter Value +} + +func (p *PropertyDescriptor) Empty() bool { + var empty PropertyDescriptor + return *p == empty +} + +func (p *PropertyDescriptor) IsAccessor() bool { + return p.Setter != nil || p.Getter != nil +} + +func (p *PropertyDescriptor) IsData() bool { + return p.Value != nil || p.Writable != FLAG_NOT_SET +} + +func (p *PropertyDescriptor) IsGeneric() bool { + return !p.IsAccessor() && !p.IsData() +} + +func (p *PropertyDescriptor) toValue(r *Runtime) Value { + if p.jsDescriptor != nil { + return p.jsDescriptor + } + if p.Empty() { + return _undefined + } + o := r.NewObject() + s := o.self + + if p.Value != nil { + s._putProp("value", p.Value, true, true, true) + } + + if p.Writable != FLAG_NOT_SET { + s._putProp("writable", valueBool(p.Writable.Bool()), true, true, true) + } + + if p.Enumerable != FLAG_NOT_SET { + s._putProp("enumerable", valueBool(p.Enumerable.Bool()), true, true, true) + } + + if p.Configurable != FLAG_NOT_SET { + s._putProp("configurable", valueBool(p.Configurable.Bool()), true, true, true) + } + + if p.Getter != nil { + s._putProp("get", p.Getter, true, true, true) + } + if p.Setter != nil { + s._putProp("set", p.Setter, true, true, true) + } + + return o +} + +func (p *PropertyDescriptor) complete() { + if p.Getter == nil && p.Setter == nil { + if p.Value == nil { + p.Value = _undefined + } + if p.Writable == FLAG_NOT_SET { + p.Writable = FLAG_FALSE + } + } else { + if p.Getter == nil { + p.Getter = _undefined + } + if p.Setter == nil { + p.Setter = _undefined + } + } + if p.Enumerable == FLAG_NOT_SET { + p.Enumerable = FLAG_FALSE + } + if p.Configurable == FLAG_NOT_SET { + p.Configurable = FLAG_FALSE + } +} + +type objectExportCacheItem map[reflect.Type]interface{} + +type objectExportCtx struct { + cache map[*Object]interface{} +} + +type objectImpl interface { + sortable + className() string + typeOf() String + getStr(p unistring.String, receiver Value) Value + getIdx(p valueInt, receiver Value) Value + getSym(p *Symbol, receiver Value) Value + + getOwnPropStr(unistring.String) Value + getOwnPropIdx(valueInt) Value + getOwnPropSym(*Symbol) Value + + setOwnStr(p unistring.String, v Value, throw bool) bool + setOwnIdx(p valueInt, v Value, throw bool) bool + setOwnSym(p *Symbol, v Value, throw bool) bool + + setForeignStr(p unistring.String, v, receiver Value, throw bool) (res bool, handled bool) + setForeignIdx(p valueInt, v, receiver Value, throw bool) (res bool, handled bool) + setForeignSym(p *Symbol, v, receiver Value, throw bool) (res bool, handled bool) + + hasPropertyStr(unistring.String) bool + hasPropertyIdx(idx valueInt) bool + hasPropertySym(s *Symbol) bool + + hasOwnPropertyStr(unistring.String) bool + hasOwnPropertyIdx(valueInt) bool + hasOwnPropertySym(s *Symbol) bool + + defineOwnPropertyStr(name unistring.String, desc PropertyDescriptor, throw bool) bool + defineOwnPropertyIdx(name valueInt, desc PropertyDescriptor, throw bool) bool + defineOwnPropertySym(name *Symbol, desc PropertyDescriptor, throw bool) bool + + deleteStr(name unistring.String, throw bool) bool + deleteIdx(idx valueInt, throw bool) bool + deleteSym(s *Symbol, throw bool) bool + + assertCallable() (call func(FunctionCall) Value, ok bool) + vmCall(vm *vm, n int) + assertConstructor() func(args []Value, newTarget *Object) *Object + proto() *Object + setProto(proto *Object, throw bool) bool + hasInstance(v Value) bool + isExtensible() bool + preventExtensions(throw bool) bool + + export(ctx *objectExportCtx) interface{} + exportType() reflect.Type + exportToMap(m reflect.Value, typ reflect.Type, ctx *objectExportCtx) error + exportToArrayOrSlice(s reflect.Value, typ reflect.Type, ctx *objectExportCtx) error + equal(objectImpl) bool + + iterateStringKeys() iterNextFunc + iterateSymbols() iterNextFunc + iterateKeys() iterNextFunc + + stringKeys(all bool, accum []Value) []Value + symbols(all bool, accum []Value) []Value + keys(all bool, accum []Value) []Value + + _putProp(name unistring.String, value Value, writable, enumerable, configurable bool) Value + _putSym(s *Symbol, prop Value) + getPrivateEnv(typ *privateEnvType, create bool) *privateElements +} + +type baseObject struct { + class string + val *Object + prototype *Object + extensible bool + + values map[unistring.String]Value + propNames []unistring.String + + lastSortedPropLen, idxPropCount int + + symValues *orderedMap + + privateElements map[*privateEnvType]*privateElements +} + +type guardedObject struct { + baseObject + guardedProps map[unistring.String]struct{} +} + +type primitiveValueObject struct { + baseObject + pValue Value +} + +func (o *primitiveValueObject) export(*objectExportCtx) interface{} { + return o.pValue.Export() +} + +func (o *primitiveValueObject) exportType() reflect.Type { + return o.pValue.ExportType() +} + +type FunctionCall struct { + This Value + Arguments []Value +} + +type ConstructorCall struct { + This *Object + Arguments []Value + NewTarget *Object +} + +func (f FunctionCall) Argument(idx int) Value { + if idx < len(f.Arguments) { + return f.Arguments[idx] + } + return _undefined +} + +func (f ConstructorCall) Argument(idx int) Value { + if idx < len(f.Arguments) { + return f.Arguments[idx] + } + return _undefined +} + +func (o *baseObject) init() { + o.values = make(map[unistring.String]Value) +} + +func (o *baseObject) className() string { + return o.class +} + +func (o *baseObject) typeOf() String { + return stringObjectC +} + +func (o *baseObject) hasPropertyStr(name unistring.String) bool { + if o.val.self.hasOwnPropertyStr(name) { + return true + } + if o.prototype != nil { + return o.prototype.self.hasPropertyStr(name) + } + return false +} + +func (o *baseObject) hasPropertyIdx(idx valueInt) bool { + return o.val.self.hasPropertyStr(idx.string()) +} + +func (o *baseObject) hasPropertySym(s *Symbol) bool { + if o.hasOwnPropertySym(s) { + return true + } + if o.prototype != nil { + return o.prototype.self.hasPropertySym(s) + } + return false +} + +func (o *baseObject) getWithOwnProp(prop, p, receiver Value) Value { + if prop == nil && o.prototype != nil { + if receiver == nil { + return o.prototype.get(p, o.val) + } + return o.prototype.get(p, receiver) + } + if prop, ok := prop.(*valueProperty); ok { + if receiver == nil { + return prop.get(o.val) + } + return prop.get(receiver) + } + return prop +} + +func (o *baseObject) getStrWithOwnProp(prop Value, name unistring.String, receiver Value) Value { + if prop == nil && o.prototype != nil { + if receiver == nil { + return o.prototype.self.getStr(name, o.val) + } + return o.prototype.self.getStr(name, receiver) + } + if prop, ok := prop.(*valueProperty); ok { + if receiver == nil { + return prop.get(o.val) + } + return prop.get(receiver) + } + return prop +} + +func (o *baseObject) getIdx(idx valueInt, receiver Value) Value { + return o.val.self.getStr(idx.string(), receiver) +} + +func (o *baseObject) getSym(s *Symbol, receiver Value) Value { + return o.getWithOwnProp(o.getOwnPropSym(s), s, receiver) +} + +func (o *baseObject) getStr(name unistring.String, receiver Value) Value { + prop := o.values[name] + if prop == nil { + if o.prototype != nil { + if receiver == nil { + return o.prototype.self.getStr(name, o.val) + } + return o.prototype.self.getStr(name, receiver) + } + } + if prop, ok := prop.(*valueProperty); ok { + if receiver == nil { + return prop.get(o.val) + } + return prop.get(receiver) + } + return prop +} + +func (o *baseObject) getOwnPropIdx(idx valueInt) Value { + return o.val.self.getOwnPropStr(idx.string()) +} + +func (o *baseObject) getOwnPropSym(s *Symbol) Value { + if o.symValues != nil { + return o.symValues.get(s) + } + return nil +} + +func (o *baseObject) getOwnPropStr(name unistring.String) Value { + return o.values[name] +} + +func (o *baseObject) checkDeleteProp(name unistring.String, prop *valueProperty, throw bool) bool { + if !prop.configurable { + if throw { + r := o.val.runtime + panic(r.NewTypeError("Cannot delete property '%s' of %s", name, r.objectproto_toString(FunctionCall{This: o.val}))) + } + return false + } + return true +} + +func (o *baseObject) checkDelete(name unistring.String, val Value, throw bool) bool { + if val, ok := val.(*valueProperty); ok { + return o.checkDeleteProp(name, val, throw) + } + return true +} + +func (o *baseObject) _delete(name unistring.String) { + delete(o.values, name) + for i, n := range o.propNames { + if n == name { + names := o.propNames + if namesMarkedForCopy(names) { + newNames := make([]unistring.String, len(names)-1, shrinkCap(len(names), cap(names))) + copy(newNames, names[:i]) + copy(newNames[i:], names[i+1:]) + o.propNames = newNames + } else { + copy(names[i:], names[i+1:]) + names[len(names)-1] = "" + o.propNames = names[:len(names)-1] + } + if i < o.lastSortedPropLen { + o.lastSortedPropLen-- + if i < o.idxPropCount { + o.idxPropCount-- + } + } + break + } + } +} + +func (o *baseObject) deleteIdx(idx valueInt, throw bool) bool { + return o.val.self.deleteStr(idx.string(), throw) +} + +func (o *baseObject) deleteSym(s *Symbol, throw bool) bool { + if o.symValues != nil { + if val := o.symValues.get(s); val != nil { + if !o.checkDelete(s.descriptiveString().string(), val, throw) { + return false + } + o.symValues.remove(s) + } + } + return true +} + +func (o *baseObject) deleteStr(name unistring.String, throw bool) bool { + if val, exists := o.values[name]; exists { + if !o.checkDelete(name, val, throw) { + return false + } + o._delete(name) + } + return true +} + +func (o *baseObject) setProto(proto *Object, throw bool) bool { + current := o.prototype + if current.SameAs(proto) { + return true + } + if !o.extensible { + o.val.runtime.typeErrorResult(throw, "%s is not extensible", o.val) + return false + } + for p := proto; p != nil; p = p.self.proto() { + if p.SameAs(o.val) { + o.val.runtime.typeErrorResult(throw, "Cyclic __proto__ value") + return false + } + if _, ok := p.self.(*proxyObject); ok { + break + } + } + o.prototype = proto + return true +} + +func (o *baseObject) setOwnStr(name unistring.String, val Value, throw bool) bool { + ownDesc := o.values[name] + if ownDesc == nil { + if proto := o.prototype; proto != nil { + // we know it's foreign because prototype loops are not allowed + if res, handled := proto.self.setForeignStr(name, val, o.val, throw); handled { + return res + } + } + // new property + if !o.extensible { + o.val.runtime.typeErrorResult(throw, "Cannot add property %s, object is not extensible", name) + return false + } else { + o.values[name] = val + names := copyNamesIfNeeded(o.propNames, 1) + o.propNames = append(names, name) + } + return true + } + if prop, ok := ownDesc.(*valueProperty); ok { + if !prop.isWritable() { + o.val.runtime.typeErrorResult(throw, "Cannot assign to read only property '%s'", name) + return false + } else { + prop.set(o.val, val) + } + } else { + o.values[name] = val + } + return true +} + +func (o *baseObject) setOwnIdx(idx valueInt, val Value, throw bool) bool { + return o.val.self.setOwnStr(idx.string(), val, throw) +} + +func (o *baseObject) setOwnSym(name *Symbol, val Value, throw bool) bool { + var ownDesc Value + if o.symValues != nil { + ownDesc = o.symValues.get(name) + } + if ownDesc == nil { + if proto := o.prototype; proto != nil { + // we know it's foreign because prototype loops are not allowed + if res, handled := proto.self.setForeignSym(name, val, o.val, throw); handled { + return res + } + } + // new property + if !o.extensible { + o.val.runtime.typeErrorResult(throw, "Cannot add property %s, object is not extensible", name) + return false + } else { + if o.symValues == nil { + o.symValues = newOrderedMap(nil) + } + o.symValues.set(name, val) + } + return true + } + if prop, ok := ownDesc.(*valueProperty); ok { + if !prop.isWritable() { + o.val.runtime.typeErrorResult(throw, "Cannot assign to read only property '%s'", name) + return false + } else { + prop.set(o.val, val) + } + } else { + o.symValues.set(name, val) + } + return true +} + +func (o *baseObject) _setForeignStr(name unistring.String, prop, val, receiver Value, throw bool) (bool, bool) { + if prop != nil { + if prop, ok := prop.(*valueProperty); ok { + if !prop.isWritable() { + o.val.runtime.typeErrorResult(throw, "Cannot assign to read only property '%s'", name) + return false, true + } + if prop.setterFunc != nil { + prop.set(receiver, val) + return true, true + } + } + } else { + if proto := o.prototype; proto != nil { + if receiver != proto { + return proto.self.setForeignStr(name, val, receiver, throw) + } + return proto.self.setOwnStr(name, val, throw), true + } + } + return false, false +} + +func (o *baseObject) _setForeignIdx(idx valueInt, prop, val, receiver Value, throw bool) (bool, bool) { + if prop != nil { + if prop, ok := prop.(*valueProperty); ok { + if !prop.isWritable() { + o.val.runtime.typeErrorResult(throw, "Cannot assign to read only property '%d'", idx) + return false, true + } + if prop.setterFunc != nil { + prop.set(receiver, val) + return true, true + } + } + } else { + if proto := o.prototype; proto != nil { + if receiver != proto { + return proto.self.setForeignIdx(idx, val, receiver, throw) + } + return proto.self.setOwnIdx(idx, val, throw), true + } + } + return false, false +} + +func (o *baseObject) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignStr(name, o.values[name], val, receiver, throw) +} + +func (o *baseObject) setForeignIdx(name valueInt, val, receiver Value, throw bool) (bool, bool) { + if idx := toIdx(name); idx != math.MaxUint32 { + o.ensurePropOrder() + if o.idxPropCount == 0 { + return o._setForeignIdx(name, name, nil, receiver, throw) + } + } + return o.setForeignStr(name.string(), val, receiver, throw) +} + +func (o *baseObject) setForeignSym(name *Symbol, val, receiver Value, throw bool) (bool, bool) { + var prop Value + if o.symValues != nil { + prop = o.symValues.get(name) + } + if prop != nil { + if prop, ok := prop.(*valueProperty); ok { + if !prop.isWritable() { + o.val.runtime.typeErrorResult(throw, "Cannot assign to read only property '%s'", name) + return false, true + } + if prop.setterFunc != nil { + prop.set(receiver, val) + return true, true + } + } + } else { + if proto := o.prototype; proto != nil { + if receiver != o.val { + return proto.self.setForeignSym(name, val, receiver, throw) + } + return proto.self.setOwnSym(name, val, throw), true + } + } + return false, false +} + +func (o *baseObject) hasOwnPropertySym(s *Symbol) bool { + if o.symValues != nil { + return o.symValues.has(s) + } + return false +} + +func (o *baseObject) hasOwnPropertyStr(name unistring.String) bool { + _, exists := o.values[name] + return exists +} + +func (o *baseObject) hasOwnPropertyIdx(idx valueInt) bool { + return o.val.self.hasOwnPropertyStr(idx.string()) +} + +func (o *baseObject) _defineOwnProperty(name unistring.String, existingValue Value, descr PropertyDescriptor, throw bool) (val Value, ok bool) { + + getterObj, _ := descr.Getter.(*Object) + setterObj, _ := descr.Setter.(*Object) + + var existing *valueProperty + + if existingValue == nil { + if !o.extensible { + o.val.runtime.typeErrorResult(throw, "Cannot define property %s, object is not extensible", name) + return nil, false + } + existing = &valueProperty{} + } else { + if existing, ok = existingValue.(*valueProperty); !ok { + existing = &valueProperty{ + writable: true, + enumerable: true, + configurable: true, + value: existingValue, + } + } + + if !existing.configurable { + if descr.Configurable == FLAG_TRUE { + goto Reject + } + if descr.Enumerable != FLAG_NOT_SET && descr.Enumerable.Bool() != existing.enumerable { + goto Reject + } + } + if existing.accessor && descr.Value != nil || !existing.accessor && (getterObj != nil || setterObj != nil) { + if !existing.configurable { + goto Reject + } + } else if !existing.accessor { + if !existing.configurable { + if !existing.writable { + if descr.Writable == FLAG_TRUE { + goto Reject + } + if descr.Value != nil && !descr.Value.SameAs(existing.value) { + goto Reject + } + } + } + } else { + if !existing.configurable { + if descr.Getter != nil && existing.getterFunc != getterObj || descr.Setter != nil && existing.setterFunc != setterObj { + goto Reject + } + } + } + } + + if descr.Writable == FLAG_TRUE && descr.Enumerable == FLAG_TRUE && descr.Configurable == FLAG_TRUE && descr.Value != nil { + return descr.Value, true + } + + if descr.Writable != FLAG_NOT_SET { + existing.writable = descr.Writable.Bool() + } + if descr.Enumerable != FLAG_NOT_SET { + existing.enumerable = descr.Enumerable.Bool() + } + if descr.Configurable != FLAG_NOT_SET { + existing.configurable = descr.Configurable.Bool() + } + + if descr.Value != nil { + existing.value = descr.Value + existing.getterFunc = nil + existing.setterFunc = nil + } + + if descr.Value != nil || descr.Writable != FLAG_NOT_SET { + existing.accessor = false + } + + if descr.Getter != nil { + existing.getterFunc = propGetter(o.val, descr.Getter, o.val.runtime) + existing.value = nil + existing.accessor = true + } + + if descr.Setter != nil { + existing.setterFunc = propSetter(o.val, descr.Setter, o.val.runtime) + existing.value = nil + existing.accessor = true + } + + if !existing.accessor && existing.value == nil { + existing.value = _undefined + } + + return existing, true + +Reject: + o.val.runtime.typeErrorResult(throw, "Cannot redefine property: %s", name) + return nil, false + +} + +func (o *baseObject) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + existingVal := o.values[name] + if v, ok := o._defineOwnProperty(name, existingVal, descr, throw); ok { + o.values[name] = v + if existingVal == nil { + names := copyNamesIfNeeded(o.propNames, 1) + o.propNames = append(names, name) + } + return true + } + return false +} + +func (o *baseObject) defineOwnPropertyIdx(idx valueInt, desc PropertyDescriptor, throw bool) bool { + return o.val.self.defineOwnPropertyStr(idx.string(), desc, throw) +} + +func (o *baseObject) defineOwnPropertySym(s *Symbol, descr PropertyDescriptor, throw bool) bool { + var existingVal Value + if o.symValues != nil { + existingVal = o.symValues.get(s) + } + if v, ok := o._defineOwnProperty(s.descriptiveString().string(), existingVal, descr, throw); ok { + if o.symValues == nil { + o.symValues = newOrderedMap(nil) + } + o.symValues.set(s, v) + return true + } + return false +} + +func (o *baseObject) _put(name unistring.String, v Value) { + if _, exists := o.values[name]; !exists { + names := copyNamesIfNeeded(o.propNames, 1) + o.propNames = append(names, name) + } + + o.values[name] = v +} + +func valueProp(value Value, writable, enumerable, configurable bool) Value { + if writable && enumerable && configurable { + return value + } + return &valueProperty{ + value: value, + writable: writable, + enumerable: enumerable, + configurable: configurable, + } +} + +func (o *baseObject) _putProp(name unistring.String, value Value, writable, enumerable, configurable bool) Value { + prop := valueProp(value, writable, enumerable, configurable) + o._put(name, prop) + return prop +} + +func (o *baseObject) _putSym(s *Symbol, prop Value) { + if o.symValues == nil { + o.symValues = newOrderedMap(nil) + } + o.symValues.set(s, prop) +} + +func (o *baseObject) getPrivateEnv(typ *privateEnvType, create bool) *privateElements { + env := o.privateElements[typ] + if env != nil && create { + panic(o.val.runtime.NewTypeError("Private fields for the class have already been set")) + } + if env == nil && create { + env = &privateElements{ + fields: make([]Value, typ.numFields), + } + if o.privateElements == nil { + o.privateElements = make(map[*privateEnvType]*privateElements) + } + o.privateElements[typ] = env + } + return env +} + +func (o *Object) tryPrimitive(methodName unistring.String) Value { + if method, ok := o.self.getStr(methodName, nil).(*Object); ok { + if call, ok := method.self.assertCallable(); ok { + v := call(FunctionCall{ + This: o, + }) + if _, fail := v.(*Object); !fail { + return v + } + } + } + return nil +} + +func (o *Object) ordinaryToPrimitiveNumber() Value { + if v := o.tryPrimitive("valueOf"); v != nil { + return v + } + + if v := o.tryPrimitive("toString"); v != nil { + return v + } + + panic(o.runtime.NewTypeError("Could not convert %v to primitive", o.self)) +} + +func (o *Object) ordinaryToPrimitiveString() Value { + if v := o.tryPrimitive("toString"); v != nil { + return v + } + + if v := o.tryPrimitive("valueOf"); v != nil { + return v + } + + panic(o.runtime.NewTypeError("Could not convert %v (%T) to primitive", o.self, o.self)) +} + +func (o *Object) tryExoticToPrimitive(hint Value) Value { + exoticToPrimitive := toMethod(o.self.getSym(SymToPrimitive, nil)) + if exoticToPrimitive != nil { + ret := exoticToPrimitive(FunctionCall{ + This: o, + Arguments: []Value{hint}, + }) + if _, fail := ret.(*Object); !fail { + return ret + } + panic(o.runtime.NewTypeError("Cannot convert object to primitive value")) + } + return nil +} + +func (o *Object) toPrimitiveNumber() Value { + if v := o.tryExoticToPrimitive(hintNumber); v != nil { + return v + } + + return o.ordinaryToPrimitiveNumber() +} + +func (o *Object) toPrimitiveString() Value { + if v := o.tryExoticToPrimitive(hintString); v != nil { + return v + } + + return o.ordinaryToPrimitiveString() +} + +func (o *Object) toPrimitive() Value { + if v := o.tryExoticToPrimitive(hintDefault); v != nil { + return v + } + return o.ordinaryToPrimitiveNumber() +} + +func (o *baseObject) assertCallable() (func(FunctionCall) Value, bool) { + return nil, false +} + +func (o *baseObject) vmCall(vm *vm, _ int) { + panic(vm.r.NewTypeError("Not a function: %s", o.val.toString())) +} + +func (o *baseObject) assertConstructor() func(args []Value, newTarget *Object) *Object { + return nil +} + +func (o *baseObject) proto() *Object { + return o.prototype +} + +func (o *baseObject) isExtensible() bool { + return o.extensible +} + +func (o *baseObject) preventExtensions(bool) bool { + o.extensible = false + return true +} + +func (o *baseObject) sortLen() int { + return toIntStrict(toLength(o.val.self.getStr("length", nil))) +} + +func (o *baseObject) sortGet(i int) Value { + return o.val.self.getIdx(valueInt(i), nil) +} + +func (o *baseObject) swap(i int, j int) { + ii := valueInt(i) + jj := valueInt(j) + + x := o.val.self.getIdx(ii, nil) + y := o.val.self.getIdx(jj, nil) + + o.val.self.setOwnIdx(ii, y, false) + o.val.self.setOwnIdx(jj, x, false) +} + +func (o *baseObject) export(ctx *objectExportCtx) interface{} { + if v, exists := ctx.get(o.val); exists { + return v + } + keys := o.stringKeys(false, nil) + m := make(map[string]interface{}, len(keys)) + ctx.put(o.val, m) + for _, itemName := range keys { + itemNameStr := itemName.String() + v := o.val.self.getStr(itemName.string(), nil) + if v != nil { + m[itemNameStr] = exportValue(v, ctx) + } else { + m[itemNameStr] = nil + } + } + + return m +} + +func (o *baseObject) exportType() reflect.Type { + return reflectTypeMap +} + +func genericExportToMap(o *Object, dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + dst.Set(reflect.MakeMap(typ)) + ctx.putTyped(o, typ, dst.Interface()) + keyTyp := typ.Key() + elemTyp := typ.Elem() + needConvertKeys := !reflectTypeString.AssignableTo(keyTyp) + iter := &enumerableIter{ + o: o, + wrapped: o.self.iterateStringKeys(), + } + r := o.runtime + for item, next := iter.next(); next != nil; item, next = next() { + var kv reflect.Value + var err error + if needConvertKeys { + kv = reflect.New(keyTyp).Elem() + err = r.toReflectValue(item.name, kv, ctx) + if err != nil { + return fmt.Errorf("could not convert map key %s to %v: %w", item.name.String(), typ, err) + } + } else { + kv = reflect.ValueOf(item.name.String()) + } + + ival := o.self.getStr(item.name.string(), nil) + if ival != nil { + vv := reflect.New(elemTyp).Elem() + err = r.toReflectValue(ival, vv, ctx) + if err != nil { + return fmt.Errorf("could not convert map value %v to %v at key %s: %w", ival, typ, item.name.String(), err) + } + dst.SetMapIndex(kv, vv) + } else { + dst.SetMapIndex(kv, reflect.Zero(elemTyp)) + } + } + + return nil +} + +func (o *baseObject) exportToMap(m reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + return genericExportToMap(o.val, m, typ, ctx) +} + +func genericExportToArrayOrSlice(o *Object, dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) (err error) { + r := o.runtime + + if method := toMethod(r.getV(o, SymIterator)); method != nil { + // iterable + + var values []Value + // cannot change (append to) the slice once it's been put into the cache, so we need to know its length beforehand + ex := r.try(func() { + values = r.iterableToList(o, method) + }) + if ex != nil { + return ex + } + if typ.Kind() == reflect.Array { + if dst.Len() != len(values) { + return fmt.Errorf("cannot convert an iterable into an array, lengths mismatch (have %d, need %d)", len(values), dst.Len()) + } + } else { + dst.Set(reflect.MakeSlice(typ, len(values), len(values))) + } + ctx.putTyped(o, typ, dst.Interface()) + for i, val := range values { + err = r.toReflectValue(val, dst.Index(i), ctx) + if err != nil { + return + } + } + } else { + // array-like + var lp Value + if _, ok := o.self.assertCallable(); !ok { + lp = o.self.getStr("length", nil) + } + if lp == nil { + return fmt.Errorf("cannot convert %v to %v: not an array or iterable", o, typ) + } + l := toIntStrict(toLength(lp)) + if dst.Len() != l { + if typ.Kind() == reflect.Array { + return fmt.Errorf("cannot convert an array-like object into an array, lengths mismatch (have %d, need %d)", l, dst.Len()) + } else { + dst.Set(reflect.MakeSlice(typ, l, l)) + } + } + ctx.putTyped(o, typ, dst.Interface()) + for i := 0; i < l; i++ { + val := nilSafe(o.self.getIdx(valueInt(i), nil)) + err = r.toReflectValue(val, dst.Index(i), ctx) + if err != nil { + return + } + } + } + + return +} + +func (o *baseObject) exportToArrayOrSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + return genericExportToArrayOrSlice(o.val, dst, typ, ctx) +} + +type enumerableFlag int + +const ( + _ENUM_UNKNOWN enumerableFlag = iota + _ENUM_FALSE + _ENUM_TRUE +) + +type propIterItem struct { + name Value + value Value + enumerable enumerableFlag +} + +type objectPropIter struct { + o *baseObject + propNames []unistring.String + idx int +} + +type recursivePropIter struct { + o objectImpl + cur iterNextFunc + seen map[unistring.String]struct{} +} + +type enumerableIter struct { + o *Object + wrapped iterNextFunc +} + +func (i *enumerableIter) next() (propIterItem, iterNextFunc) { + for { + var item propIterItem + item, i.wrapped = i.wrapped() + if i.wrapped == nil { + return item, nil + } + if item.enumerable == _ENUM_FALSE { + continue + } + if item.enumerable == _ENUM_UNKNOWN { + var prop Value + if item.value == nil { + prop = i.o.getOwnProp(item.name) + } else { + prop = item.value + } + if prop == nil { + continue + } + if prop, ok := prop.(*valueProperty); ok { + if !prop.enumerable { + continue + } + } + } + return item, i.next + } +} + +func (i *recursivePropIter) next() (propIterItem, iterNextFunc) { + for { + var item propIterItem + item, i.cur = i.cur() + if i.cur == nil { + if proto := i.o.proto(); proto != nil { + i.cur = proto.self.iterateStringKeys() + i.o = proto.self + continue + } + return propIterItem{}, nil + } + name := item.name.string() + if _, exists := i.seen[name]; !exists { + i.seen[name] = struct{}{} + return item, i.next + } + } +} + +func enumerateRecursive(o *Object) iterNextFunc { + return (&enumerableIter{ + o: o, + wrapped: (&recursivePropIter{ + o: o.self, + cur: o.self.iterateStringKeys(), + seen: make(map[unistring.String]struct{}), + }).next, + }).next +} + +func (i *objectPropIter) next() (propIterItem, iterNextFunc) { + for i.idx < len(i.propNames) { + name := i.propNames[i.idx] + i.idx++ + prop := i.o.values[name] + if prop != nil { + return propIterItem{name: stringValueFromRaw(name), value: prop}, i.next + } + } + clearNamesCopyMarker(i.propNames) + return propIterItem{}, nil +} + +var copyMarker = unistring.String(" ") + +// Set a copy-on-write flag so that any subsequent modifications of anything below the current length +// trigger a copy. +// The marker is a special value put at the index position of cap-1. Capacity is set so that the marker is +// beyond the current length (therefore invisible to normal slice operations). +// This function is called before an iteration begins to avoid copying of the names array if +// there are no modifications within the iteration. +// Note that the copying also occurs in two cases: nested iterations (on the same object) and +// iterations after a previously abandoned iteration (because there is currently no mechanism to close an +// iterator). It is still better than copying every time. +func prepareNamesForCopy(names []unistring.String) []unistring.String { + if len(names) == 0 { + return names + } + if namesMarkedForCopy(names) || cap(names) == len(names) { + var newcap int + if cap(names) == len(names) { + newcap = growCap(len(names)+1, len(names), cap(names)) + } else { + newcap = cap(names) + } + newNames := make([]unistring.String, len(names), newcap) + copy(newNames, names) + names = newNames + } + names[cap(names)-1 : cap(names)][0] = copyMarker + return names +} + +func namesMarkedForCopy(names []unistring.String) bool { + return cap(names) > len(names) && names[cap(names)-1 : cap(names)][0] == copyMarker +} + +func clearNamesCopyMarker(names []unistring.String) { + if cap(names) > len(names) { + names[cap(names)-1 : cap(names)][0] = "" + } +} + +func copyNamesIfNeeded(names []unistring.String, extraCap int) []unistring.String { + if namesMarkedForCopy(names) && len(names)+extraCap >= cap(names) { + var newcap int + newsize := len(names) + extraCap + 1 + if newsize > cap(names) { + newcap = growCap(newsize, len(names), cap(names)) + } else { + newcap = cap(names) + } + newNames := make([]unistring.String, len(names), newcap) + copy(newNames, names) + return newNames + } + return names +} + +func (o *baseObject) iterateStringKeys() iterNextFunc { + o.ensurePropOrder() + propNames := prepareNamesForCopy(o.propNames) + o.propNames = propNames + return (&objectPropIter{ + o: o, + propNames: propNames, + }).next +} + +type objectSymbolIter struct { + iter *orderedMapIter +} + +func (i *objectSymbolIter) next() (propIterItem, iterNextFunc) { + entry := i.iter.next() + if entry != nil { + return propIterItem{ + name: entry.key, + value: entry.value, + }, i.next + } + return propIterItem{}, nil +} + +func (o *baseObject) iterateSymbols() iterNextFunc { + if o.symValues != nil { + return (&objectSymbolIter{ + iter: o.symValues.newIter(), + }).next + } + return func() (propIterItem, iterNextFunc) { + return propIterItem{}, nil + } +} + +type objectAllPropIter struct { + o *Object + curStr iterNextFunc +} + +func (i *objectAllPropIter) next() (propIterItem, iterNextFunc) { + item, next := i.curStr() + if next != nil { + i.curStr = next + return item, i.next + } + return i.o.self.iterateSymbols()() +} + +func (o *baseObject) iterateKeys() iterNextFunc { + return (&objectAllPropIter{ + o: o.val, + curStr: o.val.self.iterateStringKeys(), + }).next +} + +func (o *baseObject) equal(objectImpl) bool { + // Rely on parent reference comparison + return false +} + +// hopefully this gets inlined +func (o *baseObject) ensurePropOrder() { + if o.lastSortedPropLen < len(o.propNames) { + o.fixPropOrder() + } +} + +// Reorder property names so that any integer properties are shifted to the beginning of the list +// in ascending order. This is to conform to https://262.ecma-international.org/#sec-ordinaryownpropertykeys. +// Personally I think this requirement is strange. I can sort of understand where they are coming from, +// this way arrays can be specified just as objects with a 'magic' length property. However, I think +// it's safe to assume most devs don't use Objects to store integer properties. Therefore, performing +// property type checks when adding (and potentially looking up) properties would be unreasonable. +// Instead, we keep insertion order and only change it when (if) the properties get enumerated. +func (o *baseObject) fixPropOrder() { + names := o.propNames + for i := o.lastSortedPropLen; i < len(names); i++ { + name := names[i] + if idx := strToArrayIdx(name); idx != math.MaxUint32 { + k := sort.Search(o.idxPropCount, func(j int) bool { + return strToArrayIdx(names[j]) >= idx + }) + if k < i { + if namesMarkedForCopy(names) { + newNames := make([]unistring.String, len(names), cap(names)) + copy(newNames[:k], names) + copy(newNames[k+1:i+1], names[k:i]) + copy(newNames[i+1:], names[i+1:]) + names = newNames + o.propNames = names + } else { + copy(names[k+1:i+1], names[k:i]) + } + names[k] = name + } + o.idxPropCount++ + } + } + o.lastSortedPropLen = len(names) +} + +func (o *baseObject) stringKeys(all bool, keys []Value) []Value { + o.ensurePropOrder() + if all { + for _, k := range o.propNames { + keys = append(keys, stringValueFromRaw(k)) + } + } else { + for _, k := range o.propNames { + prop := o.values[k] + if prop, ok := prop.(*valueProperty); ok && !prop.enumerable { + continue + } + keys = append(keys, stringValueFromRaw(k)) + } + } + return keys +} + +func (o *baseObject) symbols(all bool, accum []Value) []Value { + if o.symValues != nil { + iter := o.symValues.newIter() + if all { + for { + entry := iter.next() + if entry == nil { + break + } + accum = append(accum, entry.key) + } + } else { + for { + entry := iter.next() + if entry == nil { + break + } + if prop, ok := entry.value.(*valueProperty); ok { + if !prop.enumerable { + continue + } + } + accum = append(accum, entry.key) + } + } + } + + return accum +} + +func (o *baseObject) keys(all bool, accum []Value) []Value { + return o.symbols(all, o.val.self.stringKeys(all, accum)) +} + +func (o *baseObject) hasInstance(Value) bool { + panic(o.val.runtime.NewTypeError("Expecting a function in instanceof check, but got %s", o.val.toString())) +} + +func toMethod(v Value) func(FunctionCall) Value { + if v == nil || IsUndefined(v) || IsNull(v) { + return nil + } + if obj, ok := v.(*Object); ok { + if call, ok := obj.self.assertCallable(); ok { + return call + } + } + panic(newTypeError("%s is not a method", v.String())) +} + +func instanceOfOperator(o Value, c *Object) bool { + if instOfHandler := toMethod(c.self.getSym(SymHasInstance, c)); instOfHandler != nil { + return instOfHandler(FunctionCall{ + This: c, + Arguments: []Value{o}, + }).ToBoolean() + } + + return c.self.hasInstance(o) +} + +func (o *Object) get(p Value, receiver Value) Value { + switch p := p.(type) { + case valueInt: + return o.self.getIdx(p, receiver) + case *Symbol: + return o.self.getSym(p, receiver) + default: + return o.self.getStr(p.string(), receiver) + } +} + +func (o *Object) getOwnProp(p Value) Value { + switch p := p.(type) { + case valueInt: + return o.self.getOwnPropIdx(p) + case *Symbol: + return o.self.getOwnPropSym(p) + default: + return o.self.getOwnPropStr(p.string()) + } +} + +func (o *Object) hasOwnProperty(p Value) bool { + switch p := p.(type) { + case valueInt: + return o.self.hasOwnPropertyIdx(p) + case *Symbol: + return o.self.hasOwnPropertySym(p) + default: + return o.self.hasOwnPropertyStr(p.string()) + } +} + +func (o *Object) hasProperty(p Value) bool { + switch p := p.(type) { + case valueInt: + return o.self.hasPropertyIdx(p) + case *Symbol: + return o.self.hasPropertySym(p) + default: + return o.self.hasPropertyStr(p.string()) + } +} + +func (o *Object) setStr(name unistring.String, val, receiver Value, throw bool) bool { + if receiver == o { + return o.self.setOwnStr(name, val, throw) + } else { + if res, ok := o.self.setForeignStr(name, val, receiver, throw); !ok { + if robj, ok := receiver.(*Object); ok { + if prop := robj.self.getOwnPropStr(name); prop != nil { + if desc, ok := prop.(*valueProperty); ok { + if desc.accessor { + o.runtime.typeErrorResult(throw, "Receiver property %s is an accessor", name) + return false + } + if !desc.writable { + o.runtime.typeErrorResult(throw, "Cannot assign to read only property '%s'", name) + return false + } + } + return robj.self.defineOwnPropertyStr(name, PropertyDescriptor{Value: val}, throw) + } else { + return robj.self.defineOwnPropertyStr(name, PropertyDescriptor{ + Value: val, + Writable: FLAG_TRUE, + Configurable: FLAG_TRUE, + Enumerable: FLAG_TRUE, + }, throw) + } + } else { + o.runtime.typeErrorResult(throw, "Receiver is not an object: %v", receiver) + return false + } + } else { + return res + } + } +} + +func (o *Object) set(name Value, val, receiver Value, throw bool) bool { + switch name := name.(type) { + case valueInt: + return o.setIdx(name, val, receiver, throw) + case *Symbol: + return o.setSym(name, val, receiver, throw) + default: + return o.setStr(name.string(), val, receiver, throw) + } +} + +func (o *Object) setOwn(name Value, val Value, throw bool) bool { + switch name := name.(type) { + case valueInt: + return o.self.setOwnIdx(name, val, throw) + case *Symbol: + return o.self.setOwnSym(name, val, throw) + default: + return o.self.setOwnStr(name.string(), val, throw) + } +} + +func (o *Object) setIdx(name valueInt, val, receiver Value, throw bool) bool { + if receiver == o { + return o.self.setOwnIdx(name, val, throw) + } else { + if res, ok := o.self.setForeignIdx(name, val, receiver, throw); !ok { + if robj, ok := receiver.(*Object); ok { + if prop := robj.self.getOwnPropIdx(name); prop != nil { + if desc, ok := prop.(*valueProperty); ok { + if desc.accessor { + o.runtime.typeErrorResult(throw, "Receiver property %s is an accessor", name) + return false + } + if !desc.writable { + o.runtime.typeErrorResult(throw, "Cannot assign to read only property '%s'", name) + return false + } + } + robj.self.defineOwnPropertyIdx(name, PropertyDescriptor{Value: val}, throw) + } else { + robj.self.defineOwnPropertyIdx(name, PropertyDescriptor{ + Value: val, + Writable: FLAG_TRUE, + Configurable: FLAG_TRUE, + Enumerable: FLAG_TRUE, + }, throw) + } + } else { + o.runtime.typeErrorResult(throw, "Receiver is not an object: %v", receiver) + return false + } + } else { + return res + } + } + return true +} + +func (o *Object) setSym(name *Symbol, val, receiver Value, throw bool) bool { + if receiver == o { + return o.self.setOwnSym(name, val, throw) + } else { + if res, ok := o.self.setForeignSym(name, val, receiver, throw); !ok { + if robj, ok := receiver.(*Object); ok { + if prop := robj.self.getOwnPropSym(name); prop != nil { + if desc, ok := prop.(*valueProperty); ok { + if desc.accessor { + o.runtime.typeErrorResult(throw, "Receiver property %s is an accessor", name) + return false + } + if !desc.writable { + o.runtime.typeErrorResult(throw, "Cannot assign to read only property '%s'", name) + return false + } + } + robj.self.defineOwnPropertySym(name, PropertyDescriptor{Value: val}, throw) + } else { + robj.self.defineOwnPropertySym(name, PropertyDescriptor{ + Value: val, + Writable: FLAG_TRUE, + Configurable: FLAG_TRUE, + Enumerable: FLAG_TRUE, + }, throw) + } + } else { + o.runtime.typeErrorResult(throw, "Receiver is not an object: %v", receiver) + return false + } + } else { + return res + } + } + return true +} + +func (o *Object) delete(n Value, throw bool) bool { + switch n := n.(type) { + case valueInt: + return o.self.deleteIdx(n, throw) + case *Symbol: + return o.self.deleteSym(n, throw) + default: + return o.self.deleteStr(n.string(), throw) + } +} + +func (o *Object) defineOwnProperty(n Value, desc PropertyDescriptor, throw bool) bool { + switch n := n.(type) { + case valueInt: + return o.self.defineOwnPropertyIdx(n, desc, throw) + case *Symbol: + return o.self.defineOwnPropertySym(n, desc, throw) + default: + return o.self.defineOwnPropertyStr(n.string(), desc, throw) + } +} + +func (o *Object) getWeakRefs() map[weakMap]Value { + refs := o.weakRefs + if refs == nil { + refs = make(map[weakMap]Value) + o.weakRefs = refs + } + return refs +} + +func (o *Object) getId() uint64 { + id := o.id + if id == 0 { + id = o.runtime.genId() + o.id = id + } + return id +} + +func (o *guardedObject) guard(props ...unistring.String) { + if o.guardedProps == nil { + o.guardedProps = make(map[unistring.String]struct{}) + } + for _, p := range props { + o.guardedProps[p] = struct{}{} + } +} + +func (o *guardedObject) check(p unistring.String) { + if _, exists := o.guardedProps[p]; exists { + o.val.self = &o.baseObject + } +} + +func (o *guardedObject) setOwnStr(p unistring.String, v Value, throw bool) bool { + res := o.baseObject.setOwnStr(p, v, throw) + if res { + o.check(p) + } + return res +} + +func (o *guardedObject) defineOwnPropertyStr(name unistring.String, desc PropertyDescriptor, throw bool) bool { + res := o.baseObject.defineOwnPropertyStr(name, desc, throw) + if res { + o.check(name) + } + return res +} + +func (o *guardedObject) deleteStr(name unistring.String, throw bool) bool { + res := o.baseObject.deleteStr(name, throw) + if res { + o.check(name) + } + return res +} + +func (ctx *objectExportCtx) get(key *Object) (interface{}, bool) { + if v, exists := ctx.cache[key]; exists { + if item, ok := v.(objectExportCacheItem); ok { + r, exists := item[key.self.exportType()] + return r, exists + } else { + return v, true + } + } + return nil, false +} + +func (ctx *objectExportCtx) getTyped(key *Object, typ reflect.Type) (interface{}, bool) { + if v, exists := ctx.cache[key]; exists { + if item, ok := v.(objectExportCacheItem); ok { + r, exists := item[typ] + return r, exists + } else { + if reflect.TypeOf(v) == typ { + return v, true + } + } + } + return nil, false +} + +func (ctx *objectExportCtx) put(key *Object, value interface{}) { + if ctx.cache == nil { + ctx.cache = make(map[*Object]interface{}) + } + if item, ok := ctx.cache[key].(objectExportCacheItem); ok { + item[key.self.exportType()] = value + } else { + ctx.cache[key] = value + } +} + +func (ctx *objectExportCtx) putTyped(key *Object, typ reflect.Type, value interface{}) { + if ctx.cache == nil { + ctx.cache = make(map[*Object]interface{}) + } + v, exists := ctx.cache[key] + if exists { + if item, ok := ctx.cache[key].(objectExportCacheItem); ok { + item[typ] = value + } else { + m := make(objectExportCacheItem, 2) + m[key.self.exportType()] = v + m[typ] = value + ctx.cache[key] = m + } + } else { + m := make(objectExportCacheItem) + m[typ] = value + ctx.cache[key] = m + } +} + +type enumPropertiesIter struct { + o *Object + wrapped iterNextFunc +} + +func (i *enumPropertiesIter) next() (propIterItem, iterNextFunc) { + for i.wrapped != nil { + item, next := i.wrapped() + i.wrapped = next + if next == nil { + break + } + if item.value == nil { + item.value = i.o.get(item.name, nil) + if item.value == nil { + continue + } + } else { + if prop, ok := item.value.(*valueProperty); ok { + item.value = prop.get(i.o) + } + } + return item, i.next + } + return propIterItem{}, nil +} + +func iterateEnumerableProperties(o *Object) iterNextFunc { + return (&enumPropertiesIter{ + o: o, + wrapped: (&enumerableIter{ + o: o, + wrapped: o.self.iterateKeys(), + }).next, + }).next +} + +func iterateEnumerableStringProperties(o *Object) iterNextFunc { + return (&enumPropertiesIter{ + o: o, + wrapped: (&enumerableIter{ + o: o, + wrapped: o.self.iterateStringKeys(), + }).next, + }).next +} + +type privateId struct { + typ *privateEnvType + name unistring.String + idx uint32 + isMethod bool +} + +type privateEnvType struct { + numFields, numMethods uint32 +} + +type privateNames map[unistring.String]*privateId + +type privateEnv struct { + instanceType, staticType *privateEnvType + + names privateNames + + outer *privateEnv +} + +type privateElements struct { + methods []Value + fields []Value +} + +func (i *privateId) String() string { + return "#" + i.name.String() +} + +func (i *privateId) string() unistring.String { + return privateIdString(i.name) +} diff --git a/goja/object_args.go b/goja/object_args.go new file mode 100644 index 0000000..eb41d01 --- /dev/null +++ b/goja/object_args.go @@ -0,0 +1,139 @@ +package goja + +import "github.com/dop251/goja/unistring" + +type argumentsObject struct { + baseObject + length int +} + +type mappedProperty struct { + valueProperty + v *Value +} + +func (a *argumentsObject) getStr(name unistring.String, receiver Value) Value { + return a.getStrWithOwnProp(a.getOwnPropStr(name), name, receiver) +} + +func (a *argumentsObject) getOwnPropStr(name unistring.String) Value { + if mapped, ok := a.values[name].(*mappedProperty); ok { + if mapped.writable && mapped.enumerable && mapped.configurable { + return *mapped.v + } + return &valueProperty{ + value: *mapped.v, + writable: mapped.writable, + configurable: mapped.configurable, + enumerable: mapped.enumerable, + } + } + + return a.baseObject.getOwnPropStr(name) +} + +func (a *argumentsObject) init() { + a.baseObject.init() + a._putProp("length", intToValue(int64(a.length)), true, false, true) +} + +func (a *argumentsObject) setOwnStr(name unistring.String, val Value, throw bool) bool { + if prop, ok := a.values[name].(*mappedProperty); ok { + if !prop.writable { + a.val.runtime.typeErrorResult(throw, "Property is not writable: %s", name) + return false + } + *prop.v = val + return true + } + return a.baseObject.setOwnStr(name, val, throw) +} + +func (a *argumentsObject) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { + return a._setForeignStr(name, a.getOwnPropStr(name), val, receiver, throw) +} + +func (a *argumentsObject) deleteStr(name unistring.String, throw bool) bool { + if prop, ok := a.values[name].(*mappedProperty); ok { + if !a.checkDeleteProp(name, &prop.valueProperty, throw) { + return false + } + a._delete(name) + return true + } + + return a.baseObject.deleteStr(name, throw) +} + +type argumentsPropIter struct { + wrapped iterNextFunc +} + +func (i *argumentsPropIter) next() (propIterItem, iterNextFunc) { + var item propIterItem + item, i.wrapped = i.wrapped() + if i.wrapped == nil { + return propIterItem{}, nil + } + if prop, ok := item.value.(*mappedProperty); ok { + item.value = *prop.v + } + return item, i.next +} + +func (a *argumentsObject) iterateStringKeys() iterNextFunc { + return (&argumentsPropIter{ + wrapped: a.baseObject.iterateStringKeys(), + }).next +} + +func (a *argumentsObject) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + if mapped, ok := a.values[name].(*mappedProperty); ok { + existing := &valueProperty{ + configurable: mapped.configurable, + writable: true, + enumerable: mapped.enumerable, + value: *mapped.v, + } + + val, ok := a.baseObject._defineOwnProperty(name, existing, descr, throw) + if !ok { + return false + } + + if prop, ok := val.(*valueProperty); ok { + if !prop.accessor { + *mapped.v = prop.value + } + if prop.accessor || !prop.writable { + a._put(name, prop) + return true + } + mapped.configurable = prop.configurable + mapped.enumerable = prop.enumerable + } else { + *mapped.v = val + mapped.configurable = true + mapped.enumerable = true + } + + return true + } + + return a.baseObject.defineOwnPropertyStr(name, descr, throw) +} + +func (a *argumentsObject) export(ctx *objectExportCtx) interface{} { + if v, exists := ctx.get(a.val); exists { + return v + } + arr := make([]interface{}, a.length) + ctx.put(a.val, arr) + for i := range arr { + v := a.getIdx(valueInt(int64(i)), nil) + if v != nil { + arr[i] = exportValue(v, ctx) + } + } + return arr +} diff --git a/goja/object_dynamic.go b/goja/object_dynamic.go new file mode 100644 index 0000000..b1e3161 --- /dev/null +++ b/goja/object_dynamic.go @@ -0,0 +1,794 @@ +package goja + +import ( + "fmt" + "reflect" + "strconv" + + "github.com/dop251/goja/unistring" +) + +/* +DynamicObject is an interface representing a handler for a dynamic Object. Such an object can be created +using the Runtime.NewDynamicObject() method. + +Note that Runtime.ToValue() does not have any special treatment for DynamicObject. The only way to create +a dynamic object is by using the Runtime.NewDynamicObject() method. This is done deliberately to avoid +silent code breaks when this interface changes. +*/ +type DynamicObject interface { + // Get a property value for the key. May return nil if the property does not exist. + Get(key string) Value + // Set a property value for the key. Return true if success, false otherwise. + Set(key string, val Value) bool + // Has should return true if and only if the property exists. + Has(key string) bool + // Delete the property for the key. Returns true on success (note, that includes missing property). + Delete(key string) bool + // Keys returns a list of all existing property keys. There are no checks for duplicates or to make sure + // that the order conforms to https://262.ecma-international.org/#sec-ordinaryownpropertykeys + Keys() []string +} + +/* +DynamicArray is an interface representing a handler for a dynamic array Object. Such an object can be created +using the Runtime.NewDynamicArray() method. + +Any integer property key or a string property key that can be parsed into an int value (including negative +ones) is treated as an index and passed to the trap methods of the DynamicArray. Note this is different from +the regular ECMAScript arrays which only support positive indexes up to 2^32-1. + +DynamicArray cannot be sparse, i.e. hasOwnProperty(num) will return true for num >= 0 && num < Len(). Deleting +such a property is equivalent to setting it to undefined. Note that this creates a slight peculiarity because +hasOwnProperty() will still return true, even after deletion. + +Note that Runtime.ToValue() does not have any special treatment for DynamicArray. The only way to create +a dynamic array is by using the Runtime.NewDynamicArray() method. This is done deliberately to avoid +silent code breaks when this interface changes. +*/ +type DynamicArray interface { + // Len returns the current array length. + Len() int + // Get an item at index idx. Note that idx may be any integer, negative or beyond the current length. + Get(idx int) Value + // Set an item at index idx. Note that idx may be any integer, negative or beyond the current length. + // The expected behaviour when it's beyond length is that the array's length is increased to accommodate + // the item. All elements in the 'new' section of the array should be zeroed. + Set(idx int, val Value) bool + // SetLen is called when the array's 'length' property is changed. If the length is increased all elements in the + // 'new' section of the array should be zeroed. + SetLen(int) bool +} + +type baseDynamicObject struct { + val *Object + prototype *Object +} + +type dynamicObject struct { + baseDynamicObject + d DynamicObject +} + +type dynamicArray struct { + baseDynamicObject + a DynamicArray +} + +/* +NewDynamicObject creates an Object backed by the provided DynamicObject handler. + +All properties of this Object are Writable, Enumerable and Configurable data properties. Any attempt to define +a property that does not conform to this will fail. + +The Object is always extensible and cannot be made non-extensible. Object.preventExtensions() will fail. + +The Object's prototype is initially set to Object.prototype, but can be changed using regular mechanisms +(Object.SetPrototype() in Go or Object.setPrototypeOf() in JS). + +The Object cannot have own Symbol properties, however its prototype can. If you need an iterator support for +example, you could create a regular object, set Symbol.iterator on that object and then use it as a +prototype. See TestDynamicObjectCustomProto for more details. + +Export() returns the original DynamicObject. + +This mechanism is similar to ECMAScript Proxy, however because all properties are enumerable and the object +is always extensible there is no need for invariant checks which removes the need to have a target object and +makes it a lot more efficient. +*/ +func (r *Runtime) NewDynamicObject(d DynamicObject) *Object { + v := &Object{runtime: r} + o := &dynamicObject{ + d: d, + baseDynamicObject: baseDynamicObject{ + val: v, + prototype: r.global.ObjectPrototype, + }, + } + v.self = o + return v +} + +/* +NewSharedDynamicObject is similar to Runtime.NewDynamicObject but the resulting Object can be shared across multiple +Runtimes. The Object's prototype will be null. The provided DynamicObject must be goroutine-safe. +*/ +func NewSharedDynamicObject(d DynamicObject) *Object { + v := &Object{} + o := &dynamicObject{ + d: d, + baseDynamicObject: baseDynamicObject{ + val: v, + }, + } + v.self = o + return v +} + +/* +NewDynamicArray creates an array Object backed by the provided DynamicArray handler. +It is similar to NewDynamicObject, the differences are: + +- the Object is an array (i.e. Array.isArray() will return true and it will have the length property). + +- the prototype will be initially set to Array.prototype. + +- the Object cannot have any own string properties except for the 'length'. +*/ +func (r *Runtime) NewDynamicArray(a DynamicArray) *Object { + v := &Object{runtime: r} + o := &dynamicArray{ + a: a, + baseDynamicObject: baseDynamicObject{ + val: v, + prototype: r.getArrayPrototype(), + }, + } + v.self = o + return v +} + +/* +NewSharedDynamicArray is similar to Runtime.NewDynamicArray but the resulting Object can be shared across multiple +Runtimes. The Object's prototype will be null. If you need to run Array's methods on it, use Array.prototype.[...].call(a, ...). +The provided DynamicArray must be goroutine-safe. +*/ +func NewSharedDynamicArray(a DynamicArray) *Object { + v := &Object{} + o := &dynamicArray{ + a: a, + baseDynamicObject: baseDynamicObject{ + val: v, + }, + } + v.self = o + return v +} + +func (*dynamicObject) sortLen() int { + return 0 +} + +func (*dynamicObject) sortGet(i int) Value { + return nil +} + +func (*dynamicObject) swap(i int, i2 int) { +} + +func (*dynamicObject) className() string { + return classObject +} + +func (o *baseDynamicObject) getParentStr(p unistring.String, receiver Value) Value { + if proto := o.prototype; proto != nil { + if receiver == nil { + return proto.self.getStr(p, o.val) + } + return proto.self.getStr(p, receiver) + } + return nil +} + +func (o *dynamicObject) getStr(p unistring.String, receiver Value) Value { + prop := o.d.Get(p.String()) + if prop == nil { + return o.getParentStr(p, receiver) + } + return prop +} + +func (o *baseDynamicObject) getParentIdx(p valueInt, receiver Value) Value { + if proto := o.prototype; proto != nil { + if receiver == nil { + return proto.self.getIdx(p, o.val) + } + return proto.self.getIdx(p, receiver) + } + return nil +} + +func (o *dynamicObject) getIdx(p valueInt, receiver Value) Value { + prop := o.d.Get(p.String()) + if prop == nil { + return o.getParentIdx(p, receiver) + } + return prop +} + +func (o *baseDynamicObject) getSym(p *Symbol, receiver Value) Value { + if proto := o.prototype; proto != nil { + if receiver == nil { + return proto.self.getSym(p, o.val) + } + return proto.self.getSym(p, receiver) + } + return nil +} + +func (o *dynamicObject) getOwnPropStr(u unistring.String) Value { + return o.d.Get(u.String()) +} + +func (o *dynamicObject) getOwnPropIdx(v valueInt) Value { + return o.d.Get(v.String()) +} + +func (*baseDynamicObject) getOwnPropSym(*Symbol) Value { + return nil +} + +func (o *dynamicObject) _set(prop string, v Value, throw bool) bool { + if o.d.Set(prop, v) { + return true + } + typeErrorResult(throw, "'Set' on a dynamic object returned false") + return false +} + +func (o *baseDynamicObject) _setSym(throw bool) { + typeErrorResult(throw, "Dynamic objects do not support Symbol properties") +} + +func (o *dynamicObject) setOwnStr(p unistring.String, v Value, throw bool) bool { + prop := p.String() + if !o.d.Has(prop) { + if proto := o.prototype; proto != nil { + // we know it's foreign because prototype loops are not allowed + if res, handled := proto.self.setForeignStr(p, v, o.val, throw); handled { + return res + } + } + } + return o._set(prop, v, throw) +} + +func (o *dynamicObject) setOwnIdx(p valueInt, v Value, throw bool) bool { + prop := p.String() + if !o.d.Has(prop) { + if proto := o.prototype; proto != nil { + // we know it's foreign because prototype loops are not allowed + if res, handled := proto.self.setForeignIdx(p, v, o.val, throw); handled { + return res + } + } + } + return o._set(prop, v, throw) +} + +func (o *baseDynamicObject) setOwnSym(s *Symbol, v Value, throw bool) bool { + if proto := o.prototype; proto != nil { + // we know it's foreign because prototype loops are not allowed + if res, handled := proto.self.setForeignSym(s, v, o.val, throw); handled { + return res + } + } + o._setSym(throw) + return false +} + +func (o *baseDynamicObject) setParentForeignStr(p unistring.String, v, receiver Value, throw bool) (res bool, handled bool) { + if proto := o.prototype; proto != nil { + if receiver != proto { + return proto.self.setForeignStr(p, v, receiver, throw) + } + return proto.self.setOwnStr(p, v, throw), true + } + return false, false +} + +func (o *dynamicObject) setForeignStr(p unistring.String, v, receiver Value, throw bool) (res bool, handled bool) { + prop := p.String() + if !o.d.Has(prop) { + return o.setParentForeignStr(p, v, receiver, throw) + } + return false, false +} + +func (o *baseDynamicObject) setParentForeignIdx(p valueInt, v, receiver Value, throw bool) (res bool, handled bool) { + if proto := o.prototype; proto != nil { + if receiver != proto { + return proto.self.setForeignIdx(p, v, receiver, throw) + } + return proto.self.setOwnIdx(p, v, throw), true + } + return false, false +} + +func (o *dynamicObject) setForeignIdx(p valueInt, v, receiver Value, throw bool) (res bool, handled bool) { + prop := p.String() + if !o.d.Has(prop) { + return o.setParentForeignIdx(p, v, receiver, throw) + } + return false, false +} + +func (o *baseDynamicObject) setForeignSym(p *Symbol, v, receiver Value, throw bool) (res bool, handled bool) { + if proto := o.prototype; proto != nil { + if receiver != proto { + return proto.self.setForeignSym(p, v, receiver, throw) + } + return proto.self.setOwnSym(p, v, throw), true + } + return false, false +} + +func (o *dynamicObject) hasPropertyStr(u unistring.String) bool { + if o.hasOwnPropertyStr(u) { + return true + } + if proto := o.prototype; proto != nil { + return proto.self.hasPropertyStr(u) + } + return false +} + +func (o *dynamicObject) hasPropertyIdx(idx valueInt) bool { + if o.hasOwnPropertyIdx(idx) { + return true + } + if proto := o.prototype; proto != nil { + return proto.self.hasPropertyIdx(idx) + } + return false +} + +func (o *baseDynamicObject) hasPropertySym(s *Symbol) bool { + if proto := o.prototype; proto != nil { + return proto.self.hasPropertySym(s) + } + return false +} + +func (o *dynamicObject) hasOwnPropertyStr(u unistring.String) bool { + return o.d.Has(u.String()) +} + +func (o *dynamicObject) hasOwnPropertyIdx(v valueInt) bool { + return o.d.Has(v.String()) +} + +func (*baseDynamicObject) hasOwnPropertySym(_ *Symbol) bool { + return false +} + +func (o *baseDynamicObject) checkDynamicObjectPropertyDescr(name fmt.Stringer, descr PropertyDescriptor, throw bool) bool { + if descr.Getter != nil || descr.Setter != nil { + typeErrorResult(throw, "Dynamic objects do not support accessor properties") + return false + } + if descr.Writable == FLAG_FALSE { + typeErrorResult(throw, "Dynamic object field %q cannot be made read-only", name.String()) + return false + } + if descr.Enumerable == FLAG_FALSE { + typeErrorResult(throw, "Dynamic object field %q cannot be made non-enumerable", name.String()) + return false + } + if descr.Configurable == FLAG_FALSE { + typeErrorResult(throw, "Dynamic object field %q cannot be made non-configurable", name.String()) + return false + } + return true +} + +func (o *dynamicObject) defineOwnPropertyStr(name unistring.String, desc PropertyDescriptor, throw bool) bool { + if o.checkDynamicObjectPropertyDescr(name, desc, throw) { + return o._set(name.String(), desc.Value, throw) + } + return false +} + +func (o *dynamicObject) defineOwnPropertyIdx(name valueInt, desc PropertyDescriptor, throw bool) bool { + if o.checkDynamicObjectPropertyDescr(name, desc, throw) { + return o._set(name.String(), desc.Value, throw) + } + return false +} + +func (o *baseDynamicObject) defineOwnPropertySym(name *Symbol, desc PropertyDescriptor, throw bool) bool { + o._setSym(throw) + return false +} + +func (o *dynamicObject) _delete(prop string, throw bool) bool { + if o.d.Delete(prop) { + return true + } + typeErrorResult(throw, "Could not delete property %q of a dynamic object", prop) + return false +} + +func (o *dynamicObject) deleteStr(name unistring.String, throw bool) bool { + return o._delete(name.String(), throw) +} + +func (o *dynamicObject) deleteIdx(idx valueInt, throw bool) bool { + return o._delete(idx.String(), throw) +} + +func (*baseDynamicObject) deleteSym(_ *Symbol, _ bool) bool { + return true +} + +func (o *baseDynamicObject) assertCallable() (call func(FunctionCall) Value, ok bool) { + return nil, false +} + +func (o *baseDynamicObject) vmCall(vm *vm, n int) { + panic(vm.r.NewTypeError("Dynamic object is not callable")) +} + +func (*baseDynamicObject) assertConstructor() func(args []Value, newTarget *Object) *Object { + return nil +} + +func (o *baseDynamicObject) proto() *Object { + return o.prototype +} + +func (o *baseDynamicObject) setProto(proto *Object, throw bool) bool { + o.prototype = proto + return true +} + +func (o *baseDynamicObject) hasInstance(v Value) bool { + panic(newTypeError("Expecting a function in instanceof check, but got a dynamic object")) +} + +func (*baseDynamicObject) isExtensible() bool { + return true +} + +func (o *baseDynamicObject) preventExtensions(throw bool) bool { + typeErrorResult(throw, "Cannot make a dynamic object non-extensible") + return false +} + +type dynamicObjectPropIter struct { + o *dynamicObject + propNames []string + idx int +} + +func (i *dynamicObjectPropIter) next() (propIterItem, iterNextFunc) { + for i.idx < len(i.propNames) { + name := i.propNames[i.idx] + i.idx++ + if i.o.d.Has(name) { + return propIterItem{name: newStringValue(name), enumerable: _ENUM_TRUE}, i.next + } + } + return propIterItem{}, nil +} + +func (o *dynamicObject) iterateStringKeys() iterNextFunc { + keys := o.d.Keys() + return (&dynamicObjectPropIter{ + o: o, + propNames: keys, + }).next +} + +func (o *baseDynamicObject) iterateSymbols() iterNextFunc { + return func() (propIterItem, iterNextFunc) { + return propIterItem{}, nil + } +} + +func (o *dynamicObject) iterateKeys() iterNextFunc { + return o.iterateStringKeys() +} + +func (o *dynamicObject) export(ctx *objectExportCtx) interface{} { + return o.d +} + +func (o *dynamicObject) exportType() reflect.Type { + return reflect.TypeOf(o.d) +} + +func (o *baseDynamicObject) exportToMap(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + return genericExportToMap(o.val, dst, typ, ctx) +} + +func (o *baseDynamicObject) exportToArrayOrSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + return genericExportToArrayOrSlice(o.val, dst, typ, ctx) +} + +func (o *dynamicObject) equal(impl objectImpl) bool { + if other, ok := impl.(*dynamicObject); ok { + return o.d == other.d + } + return false +} + +func (o *dynamicObject) stringKeys(all bool, accum []Value) []Value { + keys := o.d.Keys() + if l := len(accum) + len(keys); l > cap(accum) { + oldAccum := accum + accum = make([]Value, len(accum), l) + copy(accum, oldAccum) + } + for _, key := range keys { + accum = append(accum, newStringValue(key)) + } + return accum +} + +func (*baseDynamicObject) symbols(all bool, accum []Value) []Value { + return accum +} + +func (o *dynamicObject) keys(all bool, accum []Value) []Value { + return o.stringKeys(all, accum) +} + +func (*baseDynamicObject) _putProp(name unistring.String, value Value, writable, enumerable, configurable bool) Value { + return nil +} + +func (*baseDynamicObject) _putSym(s *Symbol, prop Value) { +} + +func (o *baseDynamicObject) getPrivateEnv(*privateEnvType, bool) *privateElements { + panic(newTypeError("Dynamic objects cannot have private elements")) +} + +func (o *baseDynamicObject) typeOf() String { + return stringObjectC +} + +func (a *dynamicArray) sortLen() int { + return a.a.Len() +} + +func (a *dynamicArray) sortGet(i int) Value { + return a.a.Get(i) +} + +func (a *dynamicArray) swap(i int, j int) { + x := a.sortGet(i) + y := a.sortGet(j) + a.a.Set(int(i), y) + a.a.Set(int(j), x) +} + +func (a *dynamicArray) className() string { + return classArray +} + +func (a *dynamicArray) getStr(p unistring.String, receiver Value) Value { + if p == "length" { + return intToValue(int64(a.a.Len())) + } + if idx, ok := strToInt(p); ok { + return a.a.Get(idx) + } + return a.getParentStr(p, receiver) +} + +func (a *dynamicArray) getIdx(p valueInt, receiver Value) Value { + if val := a.getOwnPropIdx(p); val != nil { + return val + } + return a.getParentIdx(p, receiver) +} + +func (a *dynamicArray) getOwnPropStr(u unistring.String) Value { + if u == "length" { + return &valueProperty{ + value: intToValue(int64(a.a.Len())), + writable: true, + } + } + if idx, ok := strToInt(u); ok { + return a.a.Get(idx) + } + return nil +} + +func (a *dynamicArray) getOwnPropIdx(v valueInt) Value { + return a.a.Get(toIntStrict(int64(v))) +} + +func (a *dynamicArray) _setLen(v Value, throw bool) bool { + if a.a.SetLen(toIntStrict(v.ToInteger())) { + return true + } + typeErrorResult(throw, "'SetLen' on a dynamic array returned false") + return false +} + +func (a *dynamicArray) setOwnStr(p unistring.String, v Value, throw bool) bool { + if p == "length" { + return a._setLen(v, throw) + } + if idx, ok := strToInt(p); ok { + return a._setIdx(idx, v, throw) + } + typeErrorResult(throw, "Cannot set property %q on a dynamic array", p.String()) + return false +} + +func (a *dynamicArray) _setIdx(idx int, v Value, throw bool) bool { + if a.a.Set(idx, v) { + return true + } + typeErrorResult(throw, "'Set' on a dynamic array returned false") + return false +} + +func (a *dynamicArray) setOwnIdx(p valueInt, v Value, throw bool) bool { + return a._setIdx(toIntStrict(int64(p)), v, throw) +} + +func (a *dynamicArray) setForeignStr(p unistring.String, v, receiver Value, throw bool) (res bool, handled bool) { + return a.setParentForeignStr(p, v, receiver, throw) +} + +func (a *dynamicArray) setForeignIdx(p valueInt, v, receiver Value, throw bool) (res bool, handled bool) { + return a.setParentForeignIdx(p, v, receiver, throw) +} + +func (a *dynamicArray) hasPropertyStr(u unistring.String) bool { + if a.hasOwnPropertyStr(u) { + return true + } + if proto := a.prototype; proto != nil { + return proto.self.hasPropertyStr(u) + } + return false +} + +func (a *dynamicArray) hasPropertyIdx(idx valueInt) bool { + if a.hasOwnPropertyIdx(idx) { + return true + } + if proto := a.prototype; proto != nil { + return proto.self.hasPropertyIdx(idx) + } + return false +} + +func (a *dynamicArray) _has(idx int) bool { + return idx >= 0 && idx < a.a.Len() +} + +func (a *dynamicArray) hasOwnPropertyStr(u unistring.String) bool { + if u == "length" { + return true + } + if idx, ok := strToInt(u); ok { + return a._has(idx) + } + return false +} + +func (a *dynamicArray) hasOwnPropertyIdx(v valueInt) bool { + return a._has(toIntStrict(int64(v))) +} + +func (a *dynamicArray) defineOwnPropertyStr(name unistring.String, desc PropertyDescriptor, throw bool) bool { + if a.checkDynamicObjectPropertyDescr(name, desc, throw) { + if idx, ok := strToInt(name); ok { + return a._setIdx(idx, desc.Value, throw) + } + typeErrorResult(throw, "Cannot define property %q on a dynamic array", name.String()) + } + return false +} + +func (a *dynamicArray) defineOwnPropertyIdx(name valueInt, desc PropertyDescriptor, throw bool) bool { + if a.checkDynamicObjectPropertyDescr(name, desc, throw) { + return a._setIdx(toIntStrict(int64(name)), desc.Value, throw) + } + return false +} + +func (a *dynamicArray) _delete(idx int, throw bool) bool { + if a._has(idx) { + a._setIdx(idx, _undefined, throw) + } + return true +} + +func (a *dynamicArray) deleteStr(name unistring.String, throw bool) bool { + if idx, ok := strToInt(name); ok { + return a._delete(idx, throw) + } + if a.hasOwnPropertyStr(name) { + typeErrorResult(throw, "Cannot delete property %q on a dynamic array", name.String()) + return false + } + return true +} + +func (a *dynamicArray) deleteIdx(idx valueInt, throw bool) bool { + return a._delete(toIntStrict(int64(idx)), throw) +} + +type dynArrayPropIter struct { + a DynamicArray + idx, limit int +} + +func (i *dynArrayPropIter) next() (propIterItem, iterNextFunc) { + if i.idx < i.limit && i.idx < i.a.Len() { + name := strconv.Itoa(i.idx) + i.idx++ + return propIterItem{name: asciiString(name), enumerable: _ENUM_TRUE}, i.next + } + + return propIterItem{}, nil +} + +func (a *dynamicArray) iterateStringKeys() iterNextFunc { + return (&dynArrayPropIter{ + a: a.a, + limit: a.a.Len(), + }).next +} + +func (a *dynamicArray) iterateKeys() iterNextFunc { + return a.iterateStringKeys() +} + +func (a *dynamicArray) export(ctx *objectExportCtx) interface{} { + return a.a +} + +func (a *dynamicArray) exportType() reflect.Type { + return reflect.TypeOf(a.a) +} + +func (a *dynamicArray) equal(impl objectImpl) bool { + if other, ok := impl.(*dynamicArray); ok { + return a == other + } + return false +} + +func (a *dynamicArray) stringKeys(all bool, accum []Value) []Value { + al := a.a.Len() + l := len(accum) + al + if all { + l++ + } + if l > cap(accum) { + oldAccum := accum + accum = make([]Value, len(oldAccum), l) + copy(accum, oldAccum) + } + for i := 0; i < al; i++ { + accum = append(accum, asciiString(strconv.Itoa(i))) + } + if all { + accum = append(accum, asciiString("length")) + } + return accum +} + +func (a *dynamicArray) keys(all bool, accum []Value) []Value { + return a.stringKeys(all, accum) +} diff --git a/goja/object_dynamic_test.go b/goja/object_dynamic_test.go new file mode 100644 index 0000000..fedb8f6 --- /dev/null +++ b/goja/object_dynamic_test.go @@ -0,0 +1,420 @@ +package goja + +import ( + "sync" + "testing" +) + +type testDynObject struct { + r *Runtime + m map[string]Value +} + +func (t *testDynObject) Get(key string) Value { + return t.m[key] +} + +func (t *testDynObject) Set(key string, val Value) bool { + t.m[key] = val + return true +} + +func (t *testDynObject) Has(key string) bool { + _, exists := t.m[key] + return exists +} + +func (t *testDynObject) Delete(key string) bool { + delete(t.m, key) + return true +} + +func (t *testDynObject) Keys() []string { + keys := make([]string, 0, len(t.m)) + for k := range t.m { + keys = append(keys, k) + } + return keys +} + +type testDynArray struct { + r *Runtime + a []Value +} + +func (t *testDynArray) Len() int { + return len(t.a) +} + +func (t *testDynArray) Get(idx int) Value { + if idx < 0 { + idx += len(t.a) + } + if idx >= 0 && idx < len(t.a) { + return t.a[idx] + } + return nil +} + +func (t *testDynArray) expand(newLen int) { + if newLen > cap(t.a) { + a := make([]Value, newLen) + copy(a, t.a) + t.a = a + } else { + t.a = t.a[:newLen] + } +} + +func (t *testDynArray) Set(idx int, val Value) bool { + if idx < 0 { + idx += len(t.a) + } + if idx < 0 { + return false + } + if idx >= len(t.a) { + t.expand(idx + 1) + } + t.a[idx] = val + return true +} + +func (t *testDynArray) SetLen(i int) bool { + if i > len(t.a) { + t.expand(i) + return true + } + if i < 0 { + return false + } + if i < len(t.a) { + tail := t.a[i:len(t.a)] + for j := range tail { + tail[j] = nil + } + t.a = t.a[:i] + } + return true +} + +func TestDynamicObject(t *testing.T) { + vm := New() + dynObj := &testDynObject{ + r: vm, + m: make(map[string]Value), + } + o := vm.NewDynamicObject(dynObj) + vm.Set("o", o) + vm.testScriptWithTestLibX(` + assert(o instanceof Object, "instanceof Object"); + assert(o === o, "self equality"); + assert(o !== {}, "non-equality"); + + o.test = 42; + assert("test" in o, "'test' in o"); + assert(deepEqual(Object.getOwnPropertyDescriptor(o, "test"), {value: 42, writable: true, enumerable: true, configurable: true}), "prop desc"); + + assert.throws(TypeError, function() { + "use strict"; + Object.defineProperty(o, "test1", {value: 0, writable: false, enumerable: false, configurable: true}); + }, "define prop"); + + var keys = []; + for (var key in o) { + keys.push(key); + } + assert(compareArray(keys, ["test"]), "for-in"); + + assert(delete o.test, "delete"); + assert(!("test" in o), "'test' in o after delete"); + + assert("__proto__" in o, "__proto__ in o"); + assert.sameValue(o.__proto__, Object.prototype, "__proto__"); + o.__proto__ = null; + assert(!("__proto__" in o), "__proto__ in o after setting to null"); + `, _undefined, t) +} + +func TestDynamicObjectCustomProto(t *testing.T) { + vm := New() + m := make(map[string]Value) + dynObj := &testDynObject{ + r: vm, + m: m, + } + o := vm.NewDynamicObject(dynObj) + vm.Set("o", o) + vm.testScriptWithTestLib(` + var proto = { + valueOf: function() { + return this.num; + } + }; + proto[Symbol.toStringTag] = "GoObject"; + Object.setPrototypeOf(o, proto); + o.num = 41; + assert(o instanceof Object, "instanceof"); + assert.sameValue(o+1, 42); + assert.sameValue(o.toString(), "[object GoObject]"); + `, _undefined, t) + + if v := m["num"]; v.Export() != int64(41) { + t.Fatal(v) + } +} + +func TestDynamicArray(t *testing.T) { + vm := New() + dynObj := &testDynArray{ + r: vm, + } + a := vm.NewDynamicArray(dynObj) + vm.Set("a", a) + vm.testScriptWithTestLibX(` + assert(a instanceof Array, "instanceof Array"); + assert(a instanceof Object, "instanceof Object"); + assert(a === a, "self equality"); + assert(a !== [], "non-equality"); + assert(Array.isArray(a), "isArray()"); + assert("length" in a, "length in a"); + assert.sameValue(a.length, 0, "len == 0"); + assert.sameValue(a[0], undefined, "a[0] (1)"); + + a[0] = 0; + assert.sameValue(a[0], 0, "a[0] (2)"); + assert.sameValue(a.length, 1, "length"); + assert(deepEqual(Object.getOwnPropertyDescriptor(a, 0), {value: 0, writable: true, enumerable: true, configurable: true}), "prop desc"); + assert(deepEqual(Object.getOwnPropertyDescriptor(a, "length"), {value: 1, writable: true, enumerable: false, configurable: false}), "length prop desc"); + + assert("__proto__" in a, "__proto__ in a"); + assert.sameValue(a.__proto__, Array.prototype, "__proto__"); + + assert(compareArray(Object.keys(a), ["0"]), "Object.keys()"); + assert(compareArray(Reflect.ownKeys(a), ["0", "length"]), "Reflect.ownKeys()"); + + a.length = 2; + assert.sameValue(a.length, 2, "length after grow"); + assert.sameValue(a[1], undefined, "a[1]"); + + a[1] = 1; + assert.sameValue(a[1], 1, "a[1] after set"); + a.length = 1; + assert.sameValue(a.length, 1, "length after shrink"); + assert.sameValue(a[1], undefined, "a[1] after shrink"); + a.length = 2; + assert.sameValue(a.length, 2, "length after shrink and grow"); + assert.sameValue(a[1], undefined, "a[1] after grow"); + + a[0] = 3; a[1] = 1; a[2] = 2; + assert.sameValue(a.length, 3); + var keys = []; + for (var key in a) { + keys.push(key); + } + assert(compareArray(keys, ["0","1","2"]), "for-in"); + + var vals = []; + for (var val of a) { + vals.push(val); + } + assert(compareArray(vals, [3,1,2]), "for-of"); + + a.sort(); + assert(compareArray(a, [1,2,3]), "sort: "+a); + + assert.sameValue(a[-1], 3); + assert.sameValue(a[-4], undefined); + + assert.throws(TypeError, function() { + "use strict"; + delete a.length; + }, "delete length"); + + assert.throws(TypeError, function() { + "use strict"; + a.test = true; + }, "set string prop"); + + assert.throws(TypeError, function() { + "use strict"; + Object.defineProperty(a, 0, {value: 0, writable: false, enumerable: false, configurable: true}); + }, "define prop"); + + `, _undefined, t) +} + +type testSharedDynObject struct { + sync.RWMutex + m map[string]Value +} + +func (t *testSharedDynObject) Get(key string) Value { + t.RLock() + val := t.m[key] + t.RUnlock() + return val +} + +func (t *testSharedDynObject) Set(key string, val Value) bool { + t.Lock() + t.m[key] = val + t.Unlock() + return true +} + +func (t *testSharedDynObject) Has(key string) bool { + t.RLock() + _, exists := t.m[key] + t.RUnlock() + return exists +} + +func (t *testSharedDynObject) Delete(key string) bool { + t.Lock() + delete(t.m, key) + t.Unlock() + return true +} + +func (t *testSharedDynObject) Keys() []string { + t.RLock() + keys := make([]string, 0, len(t.m)) + for k := range t.m { + keys = append(keys, k) + } + t.RUnlock() + return keys +} + +func TestSharedDynamicObject(t *testing.T) { + dynObj := &testSharedDynObject{m: make(map[string]Value, 10000)} + o := NewSharedDynamicObject(dynObj) + ch := make(chan error, 1) + go func() { + vm := New() + vm.Set("o", o) + _, err := vm.RunString(` + for (let i = 0; i < 10000; i++) { + o[i] = i; + } + `) + ch <- err + }() + vm := New() + vm.Set("o", o) + _, err := vm.RunString(` + for (let i = 0; i < 10000; i++) { + o[i] = i+1; + } + `) + if err != nil { + t.Fatal(err) + } + + err = <-ch + if err != nil { + t.Fatal(err) + } +} + +type testSharedDynArray struct { + sync.RWMutex + a []Value +} + +func (t *testSharedDynArray) Len() int { + t.RLock() + l := len(t.a) + t.RUnlock() + return l +} + +func (t *testSharedDynArray) Get(idx int) Value { + t.RLock() + defer t.RUnlock() + if idx < 0 { + idx += len(t.a) + } + if idx >= 0 && idx < len(t.a) { + return t.a[idx] + } + return nil +} + +func (t *testSharedDynArray) expand(newLen int) { + if newLen > cap(t.a) { + a := make([]Value, newLen) + copy(a, t.a) + t.a = a + } else { + t.a = t.a[:newLen] + } +} + +func (t *testSharedDynArray) Set(idx int, val Value) bool { + t.Lock() + defer t.Unlock() + if idx < 0 { + idx += len(t.a) + } + if idx < 0 { + return false + } + if idx >= len(t.a) { + t.expand(idx + 1) + } + t.a[idx] = val + return true +} + +func (t *testSharedDynArray) SetLen(i int) bool { + t.Lock() + defer t.Unlock() + if i > len(t.a) { + t.expand(i) + return true + } + if i < 0 { + return false + } + if i < len(t.a) { + tail := t.a[i:len(t.a)] + for j := range tail { + tail[j] = nil + } + t.a = t.a[:i] + } + return true +} + +func TestSharedDynamicArray(t *testing.T) { + dynObj := &testSharedDynArray{a: make([]Value, 10000)} + o := NewSharedDynamicArray(dynObj) + ch := make(chan error, 1) + go func() { + vm := New() + vm.Set("o", o) + _, err := vm.RunString(` + for (let i = 0; i < 10000; i++) { + o[i] = i; + } + `) + ch <- err + }() + vm := New() + vm.Set("o", o) + _, err := vm.RunString(` + for (let i = 0; i < 10000; i++) { + o[i] = i+1; + } + `) + if err != nil { + t.Fatal(err) + } + + err = <-ch + if err != nil { + t.Fatal(err) + } +} diff --git a/goja/object_goarray_reflect.go b/goja/object_goarray_reflect.go new file mode 100644 index 0000000..e40364d --- /dev/null +++ b/goja/object_goarray_reflect.go @@ -0,0 +1,358 @@ +package goja + +import ( + "reflect" + "strconv" + + "github.com/dop251/goja/unistring" +) + +type objectGoArrayReflect struct { + objectGoReflect + lengthProp valueProperty + + valueCache valueArrayCache + + putIdx func(idx int, v Value, throw bool) bool +} + +type valueArrayCache []reflectValueWrapper + +func (c *valueArrayCache) get(idx int) reflectValueWrapper { + if idx < len(*c) { + return (*c)[idx] + } + return nil +} + +func (c *valueArrayCache) grow(newlen int) { + oldcap := cap(*c) + if oldcap < newlen { + a := make([]reflectValueWrapper, newlen, growCap(newlen, len(*c), oldcap)) + copy(a, *c) + *c = a + } else { + *c = (*c)[:newlen] + } +} + +func (c *valueArrayCache) put(idx int, w reflectValueWrapper) { + if len(*c) <= idx { + c.grow(idx + 1) + } + (*c)[idx] = w +} + +func (c *valueArrayCache) shrink(newlen int) { + if len(*c) > newlen { + tail := (*c)[newlen:] + for i, item := range tail { + if item != nil { + copyReflectValueWrapper(item) + tail[i] = nil + } + } + *c = (*c)[:newlen] + } +} + +func (o *objectGoArrayReflect) _init() { + o.objectGoReflect.init() + o.class = classArray + o.prototype = o.val.runtime.getArrayPrototype() + o.baseObject._put("length", &o.lengthProp) +} + +func (o *objectGoArrayReflect) init() { + o._init() + o.updateLen() + o.putIdx = o._putIdx +} + +func (o *objectGoArrayReflect) updateLen() { + o.lengthProp.value = intToValue(int64(o.fieldsValue.Len())) +} + +func (o *objectGoArrayReflect) _hasIdx(idx valueInt) bool { + if idx := int64(idx); idx >= 0 && idx < int64(o.fieldsValue.Len()) { + return true + } + return false +} + +func (o *objectGoArrayReflect) _hasStr(name unistring.String) bool { + if idx := strToIdx64(name); idx >= 0 && idx < int64(o.fieldsValue.Len()) { + return true + } + return false +} + +func (o *objectGoArrayReflect) _getIdx(idx int) Value { + if v := o.valueCache.get(idx); v != nil { + return v.esValue() + } + + v := o.fieldsValue.Index(idx) + + res, w := o.elemToValue(v) + if w != nil { + o.valueCache.put(idx, w) + } + + return res +} + +func (o *objectGoArrayReflect) getIdx(idx valueInt, receiver Value) Value { + if idx := toIntStrict(int64(idx)); idx >= 0 && idx < o.fieldsValue.Len() { + return o._getIdx(idx) + } + return o.objectGoReflect.getStr(idx.string(), receiver) +} + +func (o *objectGoArrayReflect) getStr(name unistring.String, receiver Value) Value { + var ownProp Value + if idx := strToGoIdx(name); idx >= 0 && idx < o.fieldsValue.Len() { + ownProp = o._getIdx(idx) + } else if name == "length" { + if o.fieldsValue.Kind() == reflect.Slice { + o.updateLen() + } + ownProp = &o.lengthProp + } else { + ownProp = o.objectGoReflect.getOwnPropStr(name) + } + return o.getStrWithOwnProp(ownProp, name, receiver) +} + +func (o *objectGoArrayReflect) getOwnPropStr(name unistring.String) Value { + if idx := strToGoIdx(name); idx >= 0 { + if idx < o.fieldsValue.Len() { + return &valueProperty{ + value: o._getIdx(idx), + writable: true, + enumerable: true, + } + } + return nil + } + if name == "length" { + if o.fieldsValue.Kind() == reflect.Slice { + o.updateLen() + } + return &o.lengthProp + } + return o.objectGoReflect.getOwnPropStr(name) +} + +func (o *objectGoArrayReflect) getOwnPropIdx(idx valueInt) Value { + if idx := toIntStrict(int64(idx)); idx >= 0 && idx < o.fieldsValue.Len() { + return &valueProperty{ + value: o._getIdx(idx), + writable: true, + enumerable: true, + } + } + return nil +} + +func (o *objectGoArrayReflect) _putIdx(idx int, v Value, throw bool) bool { + cached := o.valueCache.get(idx) + if cached != nil { + copyReflectValueWrapper(cached) + } + + rv := o.fieldsValue.Index(idx) + err := o.val.runtime.toReflectValue(v, rv, &objectExportCtx{}) + if err != nil { + if cached != nil { + cached.setReflectValue(rv) + } + o.val.runtime.typeErrorResult(throw, "Go type conversion error: %v", err) + return false + } + if cached != nil { + o.valueCache[idx] = nil + } + return true +} + +func (o *objectGoArrayReflect) setOwnIdx(idx valueInt, val Value, throw bool) bool { + if i := toIntStrict(int64(idx)); i >= 0 { + if i >= o.fieldsValue.Len() { + if res, ok := o._setForeignIdx(idx, nil, val, o.val, throw); ok { + return res + } + } + return o.putIdx(i, val, throw) + } else { + name := idx.string() + if res, ok := o._setForeignStr(name, nil, val, o.val, throw); !ok { + o.val.runtime.typeErrorResult(throw, "Can't set property '%s' on Go slice", name) + return false + } else { + return res + } + } +} + +func (o *objectGoArrayReflect) setOwnStr(name unistring.String, val Value, throw bool) bool { + if idx := strToGoIdx(name); idx >= 0 { + if idx >= o.fieldsValue.Len() { + if res, ok := o._setForeignStr(name, nil, val, o.val, throw); ok { + return res + } + } + return o.putIdx(idx, val, throw) + } else { + if res, ok := o._setForeignStr(name, nil, val, o.val, throw); !ok { + o.val.runtime.typeErrorResult(throw, "Can't set property '%s' on Go slice", name) + return false + } else { + return res + } + } +} + +func (o *objectGoArrayReflect) setForeignIdx(idx valueInt, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignIdx(idx, trueValIfPresent(o._hasIdx(idx)), val, receiver, throw) +} + +func (o *objectGoArrayReflect) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignStr(name, trueValIfPresent(o.hasOwnPropertyStr(name)), val, receiver, throw) +} + +func (o *objectGoArrayReflect) hasOwnPropertyIdx(idx valueInt) bool { + return o._hasIdx(idx) +} + +func (o *objectGoArrayReflect) hasOwnPropertyStr(name unistring.String) bool { + if o._hasStr(name) || name == "length" { + return true + } + return o.objectGoReflect.hasOwnPropertyStr(name) +} + +func (o *objectGoArrayReflect) defineOwnPropertyIdx(idx valueInt, descr PropertyDescriptor, throw bool) bool { + if i := toIntStrict(int64(idx)); i >= 0 { + if !o.val.runtime.checkHostObjectPropertyDescr(idx.string(), descr, throw) { + return false + } + val := descr.Value + if val == nil { + val = _undefined + } + return o.putIdx(i, val, throw) + } + o.val.runtime.typeErrorResult(throw, "Cannot define property '%d' on a Go slice", idx) + return false +} + +func (o *objectGoArrayReflect) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + if idx := strToGoIdx(name); idx >= 0 { + if !o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) { + return false + } + val := descr.Value + if val == nil { + val = _undefined + } + return o.putIdx(idx, val, throw) + } + o.val.runtime.typeErrorResult(throw, "Cannot define property '%s' on a Go slice", name) + return false +} + +func (o *objectGoArrayReflect) _deleteIdx(idx int) { + if idx < o.fieldsValue.Len() { + if cv := o.valueCache.get(idx); cv != nil { + copyReflectValueWrapper(cv) + o.valueCache[idx] = nil + } + + o.fieldsValue.Index(idx).Set(reflect.Zero(o.fieldsValue.Type().Elem())) + } +} + +func (o *objectGoArrayReflect) deleteStr(name unistring.String, throw bool) bool { + if idx := strToGoIdx(name); idx >= 0 { + o._deleteIdx(idx) + return true + } + + return o.objectGoReflect.deleteStr(name, throw) +} + +func (o *objectGoArrayReflect) deleteIdx(i valueInt, throw bool) bool { + idx := toIntStrict(int64(i)) + if idx >= 0 { + o._deleteIdx(idx) + } + return true +} + +type goArrayReflectPropIter struct { + o *objectGoArrayReflect + idx, limit int +} + +func (i *goArrayReflectPropIter) next() (propIterItem, iterNextFunc) { + if i.idx < i.limit && i.idx < i.o.fieldsValue.Len() { + name := strconv.Itoa(i.idx) + i.idx++ + return propIterItem{name: asciiString(name), enumerable: _ENUM_TRUE}, i.next + } + + return i.o.objectGoReflect.iterateStringKeys()() +} + +func (o *objectGoArrayReflect) stringKeys(all bool, accum []Value) []Value { + for i := 0; i < o.fieldsValue.Len(); i++ { + accum = append(accum, asciiString(strconv.Itoa(i))) + } + + return o.objectGoReflect.stringKeys(all, accum) +} + +func (o *objectGoArrayReflect) iterateStringKeys() iterNextFunc { + return (&goArrayReflectPropIter{ + o: o, + limit: o.fieldsValue.Len(), + }).next +} + +func (o *objectGoArrayReflect) sortLen() int { + return o.fieldsValue.Len() +} + +func (o *objectGoArrayReflect) sortGet(i int) Value { + return o.getIdx(valueInt(i), nil) +} + +func (o *objectGoArrayReflect) swap(i int, j int) { + vi := o.fieldsValue.Index(i) + vj := o.fieldsValue.Index(j) + tmp := reflect.New(o.fieldsValue.Type().Elem()).Elem() + tmp.Set(vi) + vi.Set(vj) + vj.Set(tmp) + + cachedI := o.valueCache.get(i) + cachedJ := o.valueCache.get(j) + if cachedI != nil { + cachedI.setReflectValue(vj) + o.valueCache.put(j, cachedI) + } else { + if j < len(o.valueCache) { + o.valueCache[j] = nil + } + } + + if cachedJ != nil { + cachedJ.setReflectValue(vi) + o.valueCache.put(i, cachedJ) + } else { + if i < len(o.valueCache) { + o.valueCache[i] = nil + } + } +} diff --git a/goja/object_goarray_reflect_test.go b/goja/object_goarray_reflect_test.go new file mode 100644 index 0000000..9d89aa7 --- /dev/null +++ b/goja/object_goarray_reflect_test.go @@ -0,0 +1,304 @@ +package goja + +import ( + "testing" +) + +func TestGoReflectArray(t *testing.T) { + vm := New() + vm.Set("a", [...]int{1, 2, 3}) + _, err := vm.RunString(` + if (!Array.isArray(a)) { + throw new Error("isArray() returned false"); + } + if (a[0] !== 1 || a[1] !== 2 || a[2] !== 3) { + throw new Error("Array contents is incorrect"); + } + if (!a.hasOwnProperty("length")) { + throw new Error("hasOwnProperty() returned false"); + } + let desc = Object.getOwnPropertyDescriptor(a, "length"); + if (desc.value !== 3 || desc.writable || desc.enumerable || desc.configurable) { + throw new Error("incorrect property descriptor: " + JSON.stringify(desc)); + } + `) + if err != nil { + t.Fatal(err) + } +} + +func TestGoReflectArraySort(t *testing.T) { + vm := New() + vm.Set("a", [...]int{3, 1, 2}) + v, err := vm.RunString(` + a.sort(); + if (a[0] !== 1 || a[1] !== 2 || a[2] !== 3) { + throw new Error(a.toString()); + } + a; + `) + if err != nil { + t.Fatal(err) + } + res := v.Export() + if a, ok := res.([3]int); ok { + if a[0] != 1 || a[1] != 2 || a[2] != 3 { + t.Fatal(a) + } + } else { + t.Fatalf("Wrong type: %T", res) + } +} + +func TestGoReflectArrayCopyOnChange(t *testing.T) { + vm := New() + + v, err := vm.RunString(` + a => { + let tmp = a[0]; + if (tmp !== a[0]) { + throw new Error("tmp !== a[0]"); + } + + a[0] = a[1]; + if (tmp === a[0]) { + throw new Error("tmp === a[0]"); + } + if (tmp.Test !== "1") { + throw new Error("tmp.Test: " + tmp.Test + " (" + typeof tmp.Test + ")"); + } + if (a[0].Test !== "2") { + throw new Error("a[0].Test: " + a[0].Test); + } + + a[0].Test = "3"; + if (a[0].Test !== "3") { + throw new Error("a[0].Test (1): " + a[0].Test); + } + + tmp = a[0]; + tmp.Test = "4"; + if (a[0].Test !== "4") { + throw new Error("a[0].Test (2): " + a[0].Test); + } + + delete a[0]; + if (a[0] && a[0].Test !== "") { + throw new Error("a[0].Test (3): " + a[0].Test); + } + if (tmp.Test !== "4") { + throw new Error("tmp.Test (1): " + tmp.Test); + } + + a[1] = tmp; + if (a[1].Test !== "4") { + throw new Error("a[1].Test: " + a[1].Test); + } + + // grow + tmp = a[1]; + a.push(null); + if (a.length !== 3) { + throw new Error("a.length after push: " + a.length); + } + + tmp.Test = "5"; + if (a[1].Test !== "5") { + throw new Error("a[1].Test (1): " + a[1].Test); + } + + // shrink + a.length = 1; + if (a.length !== 1) { + throw new Error("a.length after shrink: " + a.length); + } + + if (tmp.Test !== "5") { + throw new Error("tmp.Test (shrink): " + tmp.Test); + } + } + `) + if err != nil { + t.Fatal(err) + } + + fn, ok := AssertFunction(v) + if !ok { + t.Fatal("Not a function") + } + + t.Run("[]struct", func(t *testing.T) { + a := []struct { + Test string + }{{"1"}, {"2"}} + _, err := fn(nil, vm.ToValue(a)) + if err != nil { + t.Fatal(err) + } + if a[0].Test != "" { + t.Fatalf("a[0]: %#v", a[0]) + } + + if a[1].Test != "4" { + t.Fatalf("a0[1]: %#v", a[1]) + } + }) + + // The copy-on-change mechanism doesn't apply to the types below because the contained values are references. + // These tests are here for completeness and to prove that the behaviour is consistent. + + t.Run("[]I", func(t *testing.T) { + type I interface { + Get() string + } + + a := []I{&testGoReflectMethod_O{Test: "1"}, &testGoReflectMethod_O{Test: "2"}} + + _, err = fn(nil, vm.ToValue(a)) + if err != nil { + t.Fatal(err) + } + }) + + t.Run("[]interface{}", func(t *testing.T) { + a := []interface{}{&testGoReflectMethod_O{Test: "1"}, &testGoReflectMethod_O{Test: "2"}} + + _, err = fn(nil, vm.ToValue(a)) + if err != nil { + t.Fatal(err) + } + }) +} + +func TestCopyOnChangeReflectSlice(t *testing.T) { + vm := New() + v, err := vm.RunString(` + s => { + s.A.push(1); + if (s.A.length !== 1) { + throw new Error("s.A.length: " + s.A.length); + } + if (s.A[0] !== 1) { + throw new Error("s.A[0]: " + s.A[0]); + } + let tmp = s.A; + if (tmp !== s.A) { + throw new Error("tmp !== s.A"); + } + s.A = [2]; + if (tmp === s.A) { + throw new Error("tmp === s.A"); + } + if (tmp[0] !== 1) { + throw new Error("tmp[0]: " + tmp[0]); + } + if (s.A[0] !== 2) { + throw new Error("s.A[0] (1): " + s.A[0]); + } + } + `) + if err != nil { + t.Fatal(err) + } + fn, ok := AssertFunction(v) + if !ok { + t.Fatal("Not a function") + } + + t.Run("[]int", func(t *testing.T) { + type S struct { + A []int + } + var s S + _, err := fn(nil, vm.ToValue(&s)) + if err != nil { + t.Fatal(err) + } + if len(s.A) != 1 { + t.Fatal(s) + } + if s.A[0] != 2 { + t.Fatal(s.A) + } + }) + + t.Run("[]interface{}", func(t *testing.T) { + type S struct { + A []interface{} + } + var s S + _, err := fn(nil, vm.ToValue(&s)) + if err != nil { + t.Fatal(err) + } + if len(s.A) != 1 { + t.Fatal(s) + } + if s.A[0] != int64(2) { + t.Fatal(s.A) + } + }) +} + +func TestCopyOnChangeSort(t *testing.T) { + a := []struct { + Test string + }{{"2"}, {"1"}} + + vm := New() + vm.Set("a", &a) + + _, err := vm.RunString(` + let a0 = a[0]; + let a1 = a[1]; + a.sort((a, b) => a.Test.localeCompare(b.Test)); + if (a[0].Test !== "1") { + throw new Error("a[0]: " + a[0]); + } + if (a[1].Test !== "2") { + throw new Error("a[1]: " + a[1]); + } + if (a0 !== a[1]) { + throw new Error("a0 !== a[1]"); + } + if (a1 !== a[0]) { + throw new Error("a1 !== a[0]"); + } + `) + if err != nil { + t.Fatal(err) + } + + if a[0].Test != "1" || a[1].Test != "2" { + t.Fatal(a) + } +} + +type testStringerArray [8]byte + +func (a testStringerArray) String() string { + return "X" +} + +func TestReflectArrayToString(t *testing.T) { + vm := New() + var a testStringerArray + vm.Set("a", &a) + res, err := vm.RunString("`${a}`") + if err != nil { + t.Fatal(err) + } + if exp := res.Export(); exp != "X" { + t.Fatal(exp) + } + + var a1 [2]byte + vm.Set("a", &a1) + res, err = vm.RunString("`${a}`") + if err != nil { + t.Fatal(err) + } + if exp := res.Export(); exp != "0,0" { + t.Fatal(exp) + } +} diff --git a/goja/object_gomap.go b/goja/object_gomap.go new file mode 100644 index 0000000..82138c2 --- /dev/null +++ b/goja/object_gomap.go @@ -0,0 +1,158 @@ +package goja + +import ( + "reflect" + + "github.com/dop251/goja/unistring" +) + +type objectGoMapSimple struct { + baseObject + data map[string]interface{} +} + +func (o *objectGoMapSimple) init() { + o.baseObject.init() + o.prototype = o.val.runtime.global.ObjectPrototype + o.class = classObject + o.extensible = true +} + +func (o *objectGoMapSimple) _getStr(name string) Value { + v, exists := o.data[name] + if !exists { + return nil + } + return o.val.runtime.ToValue(v) +} + +func (o *objectGoMapSimple) getStr(name unistring.String, receiver Value) Value { + if v := o._getStr(name.String()); v != nil { + return v + } + return o.baseObject.getStr(name, receiver) +} + +func (o *objectGoMapSimple) getOwnPropStr(name unistring.String) Value { + if v := o._getStr(name.String()); v != nil { + return v + } + return nil +} + +func (o *objectGoMapSimple) setOwnStr(name unistring.String, val Value, throw bool) bool { + n := name.String() + if _, exists := o.data[n]; exists { + o.data[n] = val.Export() + return true + } + if proto := o.prototype; proto != nil { + // we know it's foreign because prototype loops are not allowed + if res, ok := proto.self.setForeignStr(name, val, o.val, throw); ok { + return res + } + } + // new property + if !o.extensible { + o.val.runtime.typeErrorResult(throw, "Cannot add property %s, object is not extensible", name) + return false + } else { + o.data[n] = val.Export() + } + return true +} + +func trueValIfPresent(present bool) Value { + if present { + return valueTrue + } + return nil +} + +func (o *objectGoMapSimple) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignStr(name, trueValIfPresent(o._hasStr(name.String())), val, receiver, throw) +} + +func (o *objectGoMapSimple) _hasStr(name string) bool { + _, exists := o.data[name] + return exists +} + +func (o *objectGoMapSimple) hasOwnPropertyStr(name unistring.String) bool { + return o._hasStr(name.String()) +} + +func (o *objectGoMapSimple) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + if !o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) { + return false + } + + n := name.String() + if o.extensible || o._hasStr(n) { + o.data[n] = descr.Value.Export() + return true + } + + o.val.runtime.typeErrorResult(throw, "Cannot define property %s, object is not extensible", n) + return false +} + +func (o *objectGoMapSimple) deleteStr(name unistring.String, _ bool) bool { + delete(o.data, name.String()) + return true +} + +type gomapPropIter struct { + o *objectGoMapSimple + propNames []string + idx int +} + +func (i *gomapPropIter) next() (propIterItem, iterNextFunc) { + for i.idx < len(i.propNames) { + name := i.propNames[i.idx] + i.idx++ + if _, exists := i.o.data[name]; exists { + return propIterItem{name: newStringValue(name), enumerable: _ENUM_TRUE}, i.next + } + } + + return propIterItem{}, nil +} + +func (o *objectGoMapSimple) iterateStringKeys() iterNextFunc { + propNames := make([]string, len(o.data)) + i := 0 + for key := range o.data { + propNames[i] = key + i++ + } + + return (&gomapPropIter{ + o: o, + propNames: propNames, + }).next +} + +func (o *objectGoMapSimple) stringKeys(_ bool, accum []Value) []Value { + // all own keys are enumerable + for key := range o.data { + accum = append(accum, newStringValue(key)) + } + return accum +} + +func (o *objectGoMapSimple) export(*objectExportCtx) interface{} { + return o.data +} + +func (o *objectGoMapSimple) exportType() reflect.Type { + return reflectTypeMap +} + +func (o *objectGoMapSimple) equal(other objectImpl) bool { + if other, ok := other.(*objectGoMapSimple); ok { + return o == other + } + return false +} diff --git a/goja/object_gomap_reflect.go b/goja/object_gomap_reflect.go new file mode 100644 index 0000000..d4c1a06 --- /dev/null +++ b/goja/object_gomap_reflect.go @@ -0,0 +1,294 @@ +package goja + +import ( + "fmt" + "reflect" + + "github.com/dop251/goja/unistring" +) + +type objectGoMapReflect struct { + objectGoReflect + + keyType, valueType reflect.Type +} + +func (o *objectGoMapReflect) init() { + o.objectGoReflect.init() + o.keyType = o.fieldsValue.Type().Key() + o.valueType = o.fieldsValue.Type().Elem() +} + +func (o *objectGoMapReflect) toKey(n Value, throw bool) reflect.Value { + key := reflect.New(o.keyType).Elem() + err := o.val.runtime.toReflectValue(n, key, &objectExportCtx{}) + if err != nil { + o.val.runtime.typeErrorResult(throw, "map key conversion error: %v", err) + return reflect.Value{} + } + return key +} + +func (o *objectGoMapReflect) strToKey(name string, throw bool) reflect.Value { + if o.keyType.Kind() == reflect.String { + return reflect.ValueOf(name).Convert(o.keyType) + } + return o.toKey(newStringValue(name), throw) +} + +func (o *objectGoMapReflect) _getKey(key reflect.Value) Value { + if !key.IsValid() { + return nil + } + if v := o.fieldsValue.MapIndex(key); v.IsValid() { + rv := v + if rv.Kind() == reflect.Interface { + rv = rv.Elem() + } + return o.val.runtime.toValue(v.Interface(), rv) + } + + return nil +} + +func (o *objectGoMapReflect) _get(n Value) Value { + return o._getKey(o.toKey(n, false)) +} + +func (o *objectGoMapReflect) _getStr(name string) Value { + return o._getKey(o.strToKey(name, false)) +} + +func (o *objectGoMapReflect) getStr(name unistring.String, receiver Value) Value { + if v := o._getStr(name.String()); v != nil { + return v + } + return o.objectGoReflect.getStr(name, receiver) +} + +func (o *objectGoMapReflect) getIdx(idx valueInt, receiver Value) Value { + if v := o._get(idx); v != nil { + return v + } + return o.objectGoReflect.getIdx(idx, receiver) +} + +func (o *objectGoMapReflect) getOwnPropStr(name unistring.String) Value { + if v := o._getStr(name.String()); v != nil { + return &valueProperty{ + value: v, + writable: true, + enumerable: true, + } + } + return o.objectGoReflect.getOwnPropStr(name) +} + +func (o *objectGoMapReflect) getOwnPropIdx(idx valueInt) Value { + if v := o._get(idx); v != nil { + return &valueProperty{ + value: v, + writable: true, + enumerable: true, + } + } + return o.objectGoReflect.getOwnPropStr(idx.string()) +} + +func (o *objectGoMapReflect) toValue(val Value, throw bool) (reflect.Value, bool) { + v := reflect.New(o.valueType).Elem() + err := o.val.runtime.toReflectValue(val, v, &objectExportCtx{}) + if err != nil { + o.val.runtime.typeErrorResult(throw, "map value conversion error: %v", err) + return reflect.Value{}, false + } + + return v, true +} + +func (o *objectGoMapReflect) _put(key reflect.Value, val Value, throw bool) bool { + if key.IsValid() { + if o.extensible || o.fieldsValue.MapIndex(key).IsValid() { + v, ok := o.toValue(val, throw) + if !ok { + return false + } + o.fieldsValue.SetMapIndex(key, v) + } else { + o.val.runtime.typeErrorResult(throw, "Cannot set property %v, object is not extensible", key) + return false + } + return true + } + return false +} + +func (o *objectGoMapReflect) setOwnStr(name unistring.String, val Value, throw bool) bool { + n := name.String() + key := o.strToKey(n, false) + if !key.IsValid() || !o.fieldsValue.MapIndex(key).IsValid() { + if proto := o.prototype; proto != nil { + // we know it's foreign because prototype loops are not allowed + if res, ok := proto.self.setForeignStr(name, val, o.val, throw); ok { + return res + } + } + // new property + if !o.extensible { + o.val.runtime.typeErrorResult(throw, "Cannot add property %s, object is not extensible", n) + return false + } else { + if throw && !key.IsValid() { + o.strToKey(n, true) + return false + } + } + } + o._put(key, val, throw) + return true +} + +func (o *objectGoMapReflect) setOwnIdx(idx valueInt, val Value, throw bool) bool { + key := o.toKey(idx, false) + if !key.IsValid() || !o.fieldsValue.MapIndex(key).IsValid() { + if proto := o.prototype; proto != nil { + // we know it's foreign because prototype loops are not allowed + if res, ok := proto.self.setForeignIdx(idx, val, o.val, throw); ok { + return res + } + } + // new property + if !o.extensible { + o.val.runtime.typeErrorResult(throw, "Cannot add property %d, object is not extensible", idx) + return false + } else { + if throw && !key.IsValid() { + o.toKey(idx, true) + return false + } + } + } + o._put(key, val, throw) + return true +} + +func (o *objectGoMapReflect) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignStr(name, trueValIfPresent(o.hasOwnPropertyStr(name)), val, receiver, throw) +} + +func (o *objectGoMapReflect) setForeignIdx(idx valueInt, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignIdx(idx, trueValIfPresent(o.hasOwnPropertyIdx(idx)), val, receiver, throw) +} + +func (o *objectGoMapReflect) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + if !o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) { + return false + } + + return o._put(o.strToKey(name.String(), throw), descr.Value, throw) +} + +func (o *objectGoMapReflect) defineOwnPropertyIdx(idx valueInt, descr PropertyDescriptor, throw bool) bool { + if !o.val.runtime.checkHostObjectPropertyDescr(idx.string(), descr, throw) { + return false + } + + return o._put(o.toKey(idx, throw), descr.Value, throw) +} + +func (o *objectGoMapReflect) hasOwnPropertyStr(name unistring.String) bool { + key := o.strToKey(name.String(), false) + if key.IsValid() && o.fieldsValue.MapIndex(key).IsValid() { + return true + } + return false +} + +func (o *objectGoMapReflect) hasOwnPropertyIdx(idx valueInt) bool { + key := o.toKey(idx, false) + if key.IsValid() && o.fieldsValue.MapIndex(key).IsValid() { + return true + } + return false +} + +func (o *objectGoMapReflect) deleteStr(name unistring.String, throw bool) bool { + key := o.strToKey(name.String(), throw) + if !key.IsValid() { + return false + } + o.fieldsValue.SetMapIndex(key, reflect.Value{}) + return true +} + +func (o *objectGoMapReflect) deleteIdx(idx valueInt, throw bool) bool { + key := o.toKey(idx, throw) + if !key.IsValid() { + return false + } + o.fieldsValue.SetMapIndex(key, reflect.Value{}) + return true +} + +type gomapReflectPropIter struct { + o *objectGoMapReflect + keys []reflect.Value + idx int +} + +func (i *gomapReflectPropIter) next() (propIterItem, iterNextFunc) { + for i.idx < len(i.keys) { + key := i.keys[i.idx] + v := i.o.fieldsValue.MapIndex(key) + i.idx++ + if v.IsValid() { + return propIterItem{name: i.o.keyToString(key), enumerable: _ENUM_TRUE}, i.next + } + } + + return propIterItem{}, nil +} + +func (o *objectGoMapReflect) iterateStringKeys() iterNextFunc { + return (&gomapReflectPropIter{ + o: o, + keys: o.fieldsValue.MapKeys(), + }).next +} + +func (o *objectGoMapReflect) stringKeys(_ bool, accum []Value) []Value { + // all own keys are enumerable + for _, key := range o.fieldsValue.MapKeys() { + accum = append(accum, o.keyToString(key)) + } + + return accum +} + +func (*objectGoMapReflect) keyToString(key reflect.Value) String { + kind := key.Kind() + + if kind == reflect.String { + return newStringValue(key.String()) + } + + str := fmt.Sprintf("%v", key) + + switch kind { + case reflect.Int, + reflect.Int8, + reflect.Int16, + reflect.Int32, + reflect.Int64, + reflect.Uint, + reflect.Uint8, + reflect.Uint16, + reflect.Uint32, + reflect.Uint64, + reflect.Float32, + reflect.Float64: + return asciiString(str) + default: + return newStringValue(str) + } +} diff --git a/goja/object_gomap_reflect_test.go b/goja/object_gomap_reflect_test.go new file mode 100644 index 0000000..8603c26 --- /dev/null +++ b/goja/object_gomap_reflect_test.go @@ -0,0 +1,350 @@ +package goja + +import ( + "sort" + "strings" + "testing" +) + +func TestGoMapReflectGetSet(t *testing.T) { + const SCRIPT = ` + m.c = m.a + m.b; + ` + + vm := New() + m := map[string]string{ + "a": "4", + "b": "2", + } + vm.Set("m", m) + + _, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if c := m["c"]; c != "42" { + t.Fatalf("Unexpected value: '%s'", c) + } +} + +func TestGoMapReflectIntKey(t *testing.T) { + const SCRIPT = ` + m[2] = m[0] + m[1]; + ` + + vm := New() + m := map[int]int{ + 0: 40, + 1: 2, + } + vm.Set("m", m) + + _, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if c := m[2]; c != 42 { + t.Fatalf("Unexpected value: '%d'", c) + } +} + +func TestGoMapReflectDelete(t *testing.T) { + const SCRIPT = ` + delete m.a; + ` + + vm := New() + m := map[string]string{ + "a": "4", + "b": "2", + } + vm.Set("m", m) + + _, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if _, exists := m["a"]; exists { + t.Fatal("a still exists") + } + + if b := m["b"]; b != "2" { + t.Fatalf("Unexpected b: '%s'", b) + } +} + +func TestGoMapReflectJSON(t *testing.T) { + const SCRIPT = ` + function f(m) { + return JSON.stringify(m); + } + ` + + vm := New() + m := map[string]string{ + "t": "42", + } + _, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + f := vm.Get("f") + if call, ok := AssertFunction(f); ok { + v, err := call(nil, ([]Value{vm.ToValue(m)})...) + if err != nil { + t.Fatal(err) + } + if !v.StrictEquals(asciiString(`{"t":"42"}`)) { + t.Fatalf("Unexpected value: %v", v) + } + } else { + t.Fatalf("Not a function: %v", f) + } +} + +func TestGoMapReflectProto(t *testing.T) { + const SCRIPT = ` + m.hasOwnProperty("t"); + ` + + vm := New() + m := map[string]string{ + "t": "42", + } + vm.Set("m", m) + v, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + if !v.StrictEquals(valueTrue) { + t.Fatalf("Expected true, got %v", v) + } +} + +type gomapReflect_noMethods map[string]interface{} +type gomapReflect_withMethods map[string]interface{} + +func (m gomapReflect_withMethods) Method() bool { + return true +} + +func TestGoMapReflectNoMethods(t *testing.T) { + const SCRIPT = ` + typeof m === "object" && m.hasOwnProperty("t") && m.t === 42; + ` + + vm := New() + m := make(gomapReflect_noMethods) + m["t"] = 42 + vm.Set("m", m) + v, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + if !v.StrictEquals(valueTrue) { + t.Fatalf("Expected true, got %v", v) + } + +} + +func TestGoMapReflectWithMethods(t *testing.T) { + const SCRIPT = ` + typeof m === "object" && !m.hasOwnProperty("t") && m.hasOwnProperty("Method") && m.Method(); + ` + + vm := New() + m := make(gomapReflect_withMethods) + m["t"] = 42 + vm.Set("m", m) + v, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + if !v.StrictEquals(valueTrue) { + t.Fatalf("Expected true, got %v", v) + } + +} + +func TestGoMapReflectWithProto(t *testing.T) { + vm := New() + m := map[string]string{ + "t": "42", + } + vm.Set("m", m) + vm.testScriptWithTestLib(` + (function() { + 'use strict'; + var proto = {}; + var getterAllowed = false; + var setterAllowed = false; + var tHolder = "proto t"; + Object.defineProperty(proto, "t", { + get: function() { + if (!getterAllowed) throw new Error("getter is called"); + return tHolder; + }, + set: function(v) { + if (!setterAllowed) throw new Error("setter is called"); + tHolder = v; + } + }); + var t1Holder; + Object.defineProperty(proto, "t1", { + get: function() { + return t1Holder; + }, + set: function(v) { + t1Holder = v; + } + }); + Object.setPrototypeOf(m, proto); + assert.sameValue(m.t, "42"); + m.t = 43; + assert.sameValue(m.t, "43"); + t1Holder = "test"; + assert.sameValue(m.t1, "test"); + m.t1 = "test1"; + assert.sameValue(m.t1, "test1"); + delete m.t; + getterAllowed = true; + assert.sameValue(m.t, "proto t", "after delete"); + setterAllowed = true; + m.t = true; + assert.sameValue(m.t, true, "m.t === true"); + assert.sameValue(tHolder, true, "tHolder === true"); + Object.preventExtensions(m); + assert.throws(TypeError, function() { + m.t2 = 1; + }); + m.t1 = "test2"; + assert.sameValue(m.t1, "test2"); + })(); + `, _undefined, t) +} + +func TestGoMapReflectProtoProp(t *testing.T) { + const SCRIPT = ` + (function() { + "use strict"; + var proto = {}; + Object.defineProperty(proto, "ro", {value: 42}); + Object.setPrototypeOf(m, proto); + assert.throws(TypeError, function() { + m.ro = 43; + }); + Object.defineProperty(m, "ro", {value: 43}); + assert.sameValue(m.ro, "43"); + })(); + ` + + r := New() + r.Set("m", map[string]string{}) + r.testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestGoMapReflectUnicode(t *testing.T) { + const SCRIPT = ` + Object.setPrototypeOf(m, s); + if (m.Тест !== "passed") { + throw new Error("m.Тест: " + m.Тест); + } + m["é"]; + ` + type S struct { + Тест string + } + vm := New() + m := map[string]int{ + "é": 42, + } + s := S{ + Тест: "passed", + } + vm.Set("m", m) + vm.Set("s", &s) + res, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + if res == nil || !res.StrictEquals(valueInt(42)) { + t.Fatalf("Unexpected value: %v", res) + } +} + +func TestGoMapReflectStruct(t *testing.T) { + type S struct { + Test int + } + + m := map[string]S{ + "1": {Test: 1}, + } + + vm := New() + vm.Set("m", m) + res, err := vm.RunString("m[1].Test = 2; m[1].Test") + if err != nil { + t.Fatal(err) + } + if res.Export() != int64(1) { + t.Fatal(res) + } +} + +func TestGoMapReflectElt(t *testing.T) { + type mapping map[string]interface{} + + const SCRIPT = `a.s() && a.t === null && a.t1 === undefined;` + + r := New() + + r.Set("a", mapping{ + "s": func() bool { return true }, + "t": nil, + }) + + r.testScript(SCRIPT, valueTrue, t) +} + +func TestGoMapReflectKeyToString(t *testing.T) { + vm := New() + + test := func(v any, t *testing.T) { + o1 := vm.ToValue(v).ToObject(vm) + keys := o1.Keys() + sort.Strings(keys) + if len(keys) != 2 || keys[0] != "1" || keys[1] != "2" { + t.Fatal(keys) + } + + keys1 := o1.self.stringKeys(true, nil) + sort.Slice(keys1, func(a, b int) bool { + return strings.Compare(keys1[a].String(), keys1[b].String()) < 0 + }) + if len(keys1) != 2 || keys1[0] != asciiString("1") || keys1[1] != asciiString("2") { + t.Fatal(keys1) + } + } + + t.Run("int", func(t *testing.T) { + m1 := map[int]any{ + 1: 2, + 2: 3, + } + test(m1, t) + }) + + t.Run("CustomString", func(t *testing.T) { + type CustomString string + m2 := map[CustomString]any{ + "1": 2, + "2": 3, + } + test(m2, t) + }) + +} diff --git a/goja/object_gomap_test.go b/goja/object_gomap_test.go new file mode 100644 index 0000000..81225fe --- /dev/null +++ b/goja/object_gomap_test.go @@ -0,0 +1,328 @@ +package goja + +import "testing" + +func TestGomapProp(t *testing.T) { + const SCRIPT = ` + o.a + o.b; + ` + r := New() + r.Set("o", map[string]interface{}{ + "a": 40, + "b": 2, + }) + v, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + if i := v.ToInteger(); i != 42 { + t.Fatalf("Expected 42, got: %d", i) + } +} + +func TestGomapEnumerate(t *testing.T) { + const SCRIPT = ` + var hasX = false; + var hasY = false; + for (var key in o) { + switch (key) { + case "x": + if (hasX) { + throw "Already have x"; + } + hasX = true; + break; + case "y": + if (hasY) { + throw "Already have y"; + } + hasY = true; + break; + default: + throw "Unexpected property: " + key; + } + } + hasX && hasY; + ` + r := New() + r.Set("o", map[string]interface{}{ + "x": 40, + "y": 2, + }) + v, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if !v.StrictEquals(valueTrue) { + t.Fatalf("Expected true, got %v", v) + } +} + +func TestGomapDeleteWhileEnumerate(t *testing.T) { + const SCRIPT = ` + var hasX = false; + var hasY = false; + for (var key in o) { + switch (key) { + case "x": + if (hasX) { + throw "Already have x"; + } + hasX = true; + delete o.y; + break; + case "y": + if (hasY) { + throw "Already have y"; + } + hasY = true; + delete o.x; + break; + default: + throw "Unexpected property: " + key; + } + } + hasX && !hasY || hasY && !hasX; + ` + r := New() + r.Set("o", map[string]interface{}{ + "x": 40, + "y": 2, + }) + v, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if !v.StrictEquals(valueTrue) { + t.Fatalf("Expected true, got %v", v) + } +} + +func TestGomapInstanceOf(t *testing.T) { + const SCRIPT = ` + (o instanceof Object) && !(o instanceof Error); + ` + r := New() + r.Set("o", map[string]interface{}{}) + v, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if !v.StrictEquals(valueTrue) { + t.Fatalf("Expected true, got %v", v) + } +} + +func TestGomapTypeOf(t *testing.T) { + const SCRIPT = ` + typeof o; + ` + r := New() + r.Set("o", map[string]interface{}{}) + v, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if !v.StrictEquals(asciiString("object")) { + t.Fatalf("Expected object, got %v", v) + } +} + +func TestGomapProto(t *testing.T) { + const SCRIPT = ` + o.hasOwnProperty("test"); + ` + r := New() + r.Set("o", map[string]interface{}{ + "test": 42, + }) + v, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if !v.StrictEquals(valueTrue) { + t.Fatalf("Expected true, got %v", v) + } +} + +func TestGoMapExtensibility(t *testing.T) { + const SCRIPT = ` + "use strict"; + o.test = 42; + Object.preventExtensions(o); + o.test = 43; + try { + o.test1 = 42; + } catch (e) { + if (!(e instanceof TypeError)) { + throw e; + } + } + o.test === 43 && o.test1 === undefined; + ` + + r := New() + r.Set("o", map[string]interface{}{}) + v, err := r.RunString(SCRIPT) + if err != nil { + if ex, ok := err.(*Exception); ok { + t.Fatal(ex.String()) + } else { + t.Fatal(err) + } + } + + if !v.StrictEquals(valueTrue) { + t.Fatalf("Expected true, got %v", v) + } + +} + +func TestGoMapWithProto(t *testing.T) { + vm := New() + m := map[string]interface{}{ + "t": "42", + } + vm.Set("m", m) + vm.testScriptWithTestLib(` + (function() { + 'use strict'; + var proto = {}; + var getterAllowed = false; + var setterAllowed = false; + var tHolder = "proto t"; + Object.defineProperty(proto, "t", { + get: function() { + if (!getterAllowed) throw new Error("getter is called"); + return tHolder; + }, + set: function(v) { + if (!setterAllowed) throw new Error("setter is called"); + tHolder = v; + } + }); + var t1Holder; + Object.defineProperty(proto, "t1", { + get: function() { + return t1Holder; + }, + set: function(v) { + t1Holder = v; + } + }); + Object.setPrototypeOf(m, proto); + assert.sameValue(m.t, "42"); + m.t = 43; + assert.sameValue(m.t, 43); + t1Holder = "test"; + assert.sameValue(m.t1, "test"); + m.t1 = "test1"; + assert.sameValue(m.t1, "test1"); + delete m.t; + getterAllowed = true; + assert.sameValue(m.t, "proto t", "after delete"); + setterAllowed = true; + m.t = true; + assert.sameValue(m.t, true); + assert.sameValue(tHolder, true); + Object.preventExtensions(m); + assert.throws(TypeError, function() { + m.t2 = 1; + }); + m.t1 = "test2"; + assert.sameValue(m.t1, "test2"); + })(); + `, _undefined, t) +} + +func TestGoMapProtoProp(t *testing.T) { + const SCRIPT = ` + (function() { + "use strict"; + var proto = {}; + Object.defineProperty(proto, "ro", {value: 42}); + Object.setPrototypeOf(m, proto); + assert.throws(TypeError, function() { + m.ro = 43; + }); + Object.defineProperty(m, "ro", {value: 43}); + assert.sameValue(m.ro, 43); + })(); + ` + + r := New() + r.Set("m", map[string]interface{}{}) + r.testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestGoMapProtoPropChain(t *testing.T) { + const SCRIPT = ` + (function() { + "use strict"; + var p1 = Object.create(null); + m.__proto__ = p1; + + Object.defineProperty(p1, "test", { + value: 42 + }); + + Object.defineProperty(m, "test", { + value: 43, + writable: true, + }); + var o = Object.create(m); + o.test = 44; + assert.sameValue(o.test, 44); + + var sym = Symbol(true); + Object.defineProperty(p1, sym, { + value: 42 + }); + + Object.defineProperty(m, sym, { + value: 43, + writable: true, + }); + o[sym] = 44; + assert.sameValue(o[sym], 44); + })(); + ` + + r := New() + r.Set("m", map[string]interface{}{}) + r.testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestGoMapUnicode(t *testing.T) { + const SCRIPT = ` + Object.setPrototypeOf(m, s); + if (m.Тест !== "passed") { + throw new Error("m.Тест: " + m.Тест); + } + m["é"]; + ` + type S struct { + Тест string + } + vm := New() + m := map[string]interface{}{ + "é": 42, + } + s := S{ + Тест: "passed", + } + vm.Set("m", m) + vm.Set("s", &s) + res, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + if res == nil || !res.StrictEquals(valueInt(42)) { + t.Fatalf("Unexpected value: %v", res) + } +} diff --git a/goja/object_goreflect.go b/goja/object_goreflect.go new file mode 100644 index 0000000..f8ca6d0 --- /dev/null +++ b/goja/object_goreflect.go @@ -0,0 +1,674 @@ +package goja + +import ( + "fmt" + "go/ast" + "reflect" + "strings" + + "github.com/dop251/goja/parser" + "github.com/dop251/goja/unistring" +) + +// JsonEncodable allows custom JSON encoding by JSON.stringify() +// Note that if the returned value itself also implements JsonEncodable, it won't have any effect. +type JsonEncodable interface { + JsonEncodable() interface{} +} + +// FieldNameMapper provides custom mapping between Go and JavaScript property names. +type FieldNameMapper interface { + // FieldName returns a JavaScript name for the given struct field in the given type. + // If this method returns "" the field becomes hidden. + FieldName(t reflect.Type, f reflect.StructField) string + + // MethodName returns a JavaScript name for the given method in the given type. + // If this method returns "" the method becomes hidden. + MethodName(t reflect.Type, m reflect.Method) string +} + +type tagFieldNameMapper struct { + tagName string + uncapMethods bool +} + +func (tfm tagFieldNameMapper) FieldName(_ reflect.Type, f reflect.StructField) string { + tag := f.Tag.Get(tfm.tagName) + if idx := strings.IndexByte(tag, ','); idx != -1 { + tag = tag[:idx] + } + if parser.IsIdentifier(tag) { + return tag + } + return "" +} + +func uncapitalize(s string) string { + return strings.ToLower(s[0:1]) + s[1:] +} + +func (tfm tagFieldNameMapper) MethodName(_ reflect.Type, m reflect.Method) string { + if tfm.uncapMethods { + return uncapitalize(m.Name) + } + return m.Name +} + +type uncapFieldNameMapper struct { +} + +func (u uncapFieldNameMapper) FieldName(_ reflect.Type, f reflect.StructField) string { + return uncapitalize(f.Name) +} + +func (u uncapFieldNameMapper) MethodName(_ reflect.Type, m reflect.Method) string { + return uncapitalize(m.Name) +} + +type reflectFieldInfo struct { + Index []int + Anonymous bool +} + +type reflectFieldsInfo struct { + Fields map[string]reflectFieldInfo + Names []string +} + +type reflectMethodsInfo struct { + Methods map[string]int + Names []string +} + +type reflectValueWrapper interface { + esValue() Value + reflectValue() reflect.Value + setReflectValue(reflect.Value) +} + +func isContainer(k reflect.Kind) bool { + switch k { + case reflect.Struct, reflect.Slice, reflect.Array: + return true + } + return false +} + +func copyReflectValueWrapper(w reflectValueWrapper) { + v := w.reflectValue() + c := reflect.New(v.Type()).Elem() + c.Set(v) + w.setReflectValue(c) +} + +type objectGoReflect struct { + baseObject + origValue, fieldsValue reflect.Value + + fieldsInfo *reflectFieldsInfo + methodsInfo *reflectMethodsInfo + + methodsValue reflect.Value + + valueCache map[string]reflectValueWrapper + + toString, valueOf func() Value + + toJson func() interface{} +} + +func (o *objectGoReflect) init() { + o.baseObject.init() + switch o.fieldsValue.Kind() { + case reflect.Bool: + o.class = classBoolean + o.prototype = o.val.runtime.getBooleanPrototype() + o.toString = o._toStringBool + o.valueOf = o._valueOfBool + case reflect.String: + o.class = classString + o.prototype = o.val.runtime.getStringPrototype() + o.toString = o._toStringString + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + o.class = classNumber + o.prototype = o.val.runtime.getNumberPrototype() + o.valueOf = o._valueOfInt + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + o.class = classNumber + o.prototype = o.val.runtime.getNumberPrototype() + o.valueOf = o._valueOfUint + case reflect.Float32, reflect.Float64: + o.class = classNumber + o.prototype = o.val.runtime.getNumberPrototype() + o.valueOf = o._valueOfFloat + default: + o.class = classObject + o.prototype = o.val.runtime.global.ObjectPrototype + } + + if o.fieldsValue.Kind() == reflect.Struct { + o.fieldsInfo = o.val.runtime.fieldsInfo(o.fieldsValue.Type()) + } + + var methodsType reflect.Type + // Always use pointer type for non-interface values to be able to access both methods defined on + // the literal type and on the pointer. + if o.fieldsValue.Kind() != reflect.Interface { + methodsType = reflect.PtrTo(o.fieldsValue.Type()) + } else { + methodsType = o.fieldsValue.Type() + } + + o.methodsInfo = o.val.runtime.methodsInfo(methodsType) + + // Container values and values that have at least one method defined on the pointer type + // need to be addressable. + if !o.origValue.CanAddr() && (isContainer(o.origValue.Kind()) || len(o.methodsInfo.Names) > 0) { + value := reflect.New(o.origValue.Type()).Elem() + value.Set(o.origValue) + o.origValue = value + if value.Kind() != reflect.Ptr { + o.fieldsValue = value + } + } + + o.extensible = true + + switch o.origValue.Interface().(type) { + case fmt.Stringer: + o.toString = o._toStringStringer + case error: + o.toString = o._toStringError + } + + if len(o.methodsInfo.Names) > 0 && o.fieldsValue.Kind() != reflect.Interface { + o.methodsValue = o.fieldsValue.Addr() + } else { + o.methodsValue = o.fieldsValue + } + + if j, ok := o.origValue.Interface().(JsonEncodable); ok { + o.toJson = j.JsonEncodable + } +} + +func (o *objectGoReflect) getStr(name unistring.String, receiver Value) Value { + if v := o._get(name.String()); v != nil { + return v + } + return o.baseObject.getStr(name, receiver) +} + +func (o *objectGoReflect) _getField(jsName string) reflect.Value { + if o.fieldsInfo != nil { + if info, exists := o.fieldsInfo.Fields[jsName]; exists { + return o.fieldsValue.FieldByIndex(info.Index) + } + } + + return reflect.Value{} +} + +func (o *objectGoReflect) _getMethod(jsName string) reflect.Value { + if o.methodsInfo != nil { + if idx, exists := o.methodsInfo.Methods[jsName]; exists { + return o.methodsValue.Method(idx) + } + } + + return reflect.Value{} +} + +func (o *objectGoReflect) elemToValue(ev reflect.Value) (Value, reflectValueWrapper) { + if isContainer(ev.Kind()) { + ret := o.val.runtime.toValue(ev.Interface(), ev) + if obj, ok := ret.(*Object); ok { + if w, ok := obj.self.(reflectValueWrapper); ok { + return ret, w + } + } + return ret, nil + } + + if ev.Kind() == reflect.Interface { + ev = ev.Elem() + } + + if ev.Kind() == reflect.Invalid { + return _null, nil + } + + return o.val.runtime.toValue(ev.Interface(), ev), nil +} + +func (o *objectGoReflect) _getFieldValue(name string) Value { + if v := o.valueCache[name]; v != nil { + return v.esValue() + } + if v := o._getField(name); v.IsValid() { + res, w := o.elemToValue(v) + if w != nil { + if o.valueCache == nil { + o.valueCache = make(map[string]reflectValueWrapper) + } + o.valueCache[name] = w + } + return res + } + return nil +} + +func (o *objectGoReflect) _get(name string) Value { + if o.fieldsValue.Kind() == reflect.Struct { + if ret := o._getFieldValue(name); ret != nil { + return ret + } + } + + if v := o._getMethod(name); v.IsValid() { + return o.val.runtime.toValue(v.Interface(), v) + } + + return nil +} + +func (o *objectGoReflect) getOwnPropStr(name unistring.String) Value { + n := name.String() + if o.fieldsValue.Kind() == reflect.Struct { + if v := o._getFieldValue(n); v != nil { + return &valueProperty{ + value: v, + writable: true, + enumerable: true, + } + } + } + + if v := o._getMethod(n); v.IsValid() { + return &valueProperty{ + value: o.val.runtime.toValue(v.Interface(), v), + enumerable: true, + } + } + + return o.baseObject.getOwnPropStr(name) +} + +func (o *objectGoReflect) setOwnStr(name unistring.String, val Value, throw bool) bool { + has, ok := o._put(name.String(), val, throw) + if !has { + if res, ok := o._setForeignStr(name, nil, val, o.val, throw); !ok { + o.val.runtime.typeErrorResult(throw, "Cannot assign to property %s of a host object", name) + return false + } else { + return res + } + } + return ok +} + +func (o *objectGoReflect) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignStr(name, trueValIfPresent(o._has(name.String())), val, receiver, throw) +} + +func (o *objectGoReflect) setForeignIdx(idx valueInt, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignIdx(idx, nil, val, receiver, throw) +} + +func (o *objectGoReflect) _put(name string, val Value, throw bool) (has, ok bool) { + if o.fieldsValue.Kind() == reflect.Struct { + if v := o._getField(name); v.IsValid() { + cached := o.valueCache[name] + if cached != nil { + copyReflectValueWrapper(cached) + } + + err := o.val.runtime.toReflectValue(val, v, &objectExportCtx{}) + if err != nil { + if cached != nil { + cached.setReflectValue(v) + } + o.val.runtime.typeErrorResult(throw, "Go struct conversion error: %v", err) + return true, false + } + if cached != nil { + delete(o.valueCache, name) + } + return true, true + } + } + return false, false +} + +func (o *objectGoReflect) _putProp(name unistring.String, value Value, writable, enumerable, configurable bool) Value { + if _, ok := o._put(name.String(), value, false); ok { + return value + } + return o.baseObject._putProp(name, value, writable, enumerable, configurable) +} + +func (r *Runtime) checkHostObjectPropertyDescr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + if descr.Getter != nil || descr.Setter != nil { + r.typeErrorResult(throw, "Host objects do not support accessor properties") + return false + } + if descr.Writable == FLAG_FALSE { + r.typeErrorResult(throw, "Host object field %s cannot be made read-only", name) + return false + } + if descr.Configurable == FLAG_TRUE { + r.typeErrorResult(throw, "Host object field %s cannot be made configurable", name) + return false + } + return true +} + +func (o *objectGoReflect) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + if o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) { + n := name.String() + if has, ok := o._put(n, descr.Value, throw); !has { + o.val.runtime.typeErrorResult(throw, "Cannot define property '%s' on a host object", n) + return false + } else { + return ok + } + } + return false +} + +func (o *objectGoReflect) _has(name string) bool { + if o.fieldsValue.Kind() == reflect.Struct { + if v := o._getField(name); v.IsValid() { + return true + } + } + if v := o._getMethod(name); v.IsValid() { + return true + } + return false +} + +func (o *objectGoReflect) hasOwnPropertyStr(name unistring.String) bool { + return o._has(name.String()) || o.baseObject.hasOwnPropertyStr(name) +} + +func (o *objectGoReflect) _valueOfInt() Value { + return intToValue(o.fieldsValue.Int()) +} + +func (o *objectGoReflect) _valueOfUint() Value { + return intToValue(int64(o.fieldsValue.Uint())) +} + +func (o *objectGoReflect) _valueOfBool() Value { + if o.fieldsValue.Bool() { + return valueTrue + } else { + return valueFalse + } +} + +func (o *objectGoReflect) _valueOfFloat() Value { + return floatToValue(o.fieldsValue.Float()) +} + +func (o *objectGoReflect) _toStringStringer() Value { + return newStringValue(o.origValue.Interface().(fmt.Stringer).String()) +} + +func (o *objectGoReflect) _toStringString() Value { + return newStringValue(o.fieldsValue.String()) +} + +func (o *objectGoReflect) _toStringBool() Value { + if o.fieldsValue.Bool() { + return stringTrue + } else { + return stringFalse + } +} + +func (o *objectGoReflect) _toStringError() Value { + return newStringValue(o.origValue.Interface().(error).Error()) +} + +func (o *objectGoReflect) deleteStr(name unistring.String, throw bool) bool { + n := name.String() + if o._has(n) { + o.val.runtime.typeErrorResult(throw, "Cannot delete property %s from a Go type", n) + return false + } + return o.baseObject.deleteStr(name, throw) +} + +type goreflectPropIter struct { + o *objectGoReflect + idx int +} + +func (i *goreflectPropIter) nextField() (propIterItem, iterNextFunc) { + names := i.o.fieldsInfo.Names + if i.idx < len(names) { + name := names[i.idx] + i.idx++ + return propIterItem{name: newStringValue(name), enumerable: _ENUM_TRUE}, i.nextField + } + + i.idx = 0 + return i.nextMethod() +} + +func (i *goreflectPropIter) nextMethod() (propIterItem, iterNextFunc) { + names := i.o.methodsInfo.Names + if i.idx < len(names) { + name := names[i.idx] + i.idx++ + return propIterItem{name: newStringValue(name), enumerable: _ENUM_TRUE}, i.nextMethod + } + + return propIterItem{}, nil +} + +func (o *objectGoReflect) iterateStringKeys() iterNextFunc { + r := &goreflectPropIter{ + o: o, + } + if o.fieldsInfo != nil { + return r.nextField + } + + return r.nextMethod +} + +func (o *objectGoReflect) stringKeys(_ bool, accum []Value) []Value { + // all own keys are enumerable + if o.fieldsInfo != nil { + for _, name := range o.fieldsInfo.Names { + accum = append(accum, newStringValue(name)) + } + } + + for _, name := range o.methodsInfo.Names { + accum = append(accum, newStringValue(name)) + } + + return accum +} + +func (o *objectGoReflect) export(*objectExportCtx) interface{} { + return o.origValue.Interface() +} + +func (o *objectGoReflect) exportType() reflect.Type { + return o.origValue.Type() +} + +func (o *objectGoReflect) equal(other objectImpl) bool { + if other, ok := other.(*objectGoReflect); ok { + k1, k2 := o.fieldsValue.Kind(), other.fieldsValue.Kind() + if k1 == k2 { + if isContainer(k1) { + return o.fieldsValue == other.fieldsValue + } + return o.fieldsValue.Interface() == other.fieldsValue.Interface() + } + } + return false +} + +func (o *objectGoReflect) reflectValue() reflect.Value { + return o.fieldsValue +} + +func (o *objectGoReflect) setReflectValue(v reflect.Value) { + o.fieldsValue = v + o.origValue = v + o.methodsValue = v.Addr() +} + +func (o *objectGoReflect) esValue() Value { + return o.val +} + +func (r *Runtime) buildFieldInfo(t reflect.Type, index []int, info *reflectFieldsInfo) { + n := t.NumField() + for i := 0; i < n; i++ { + field := t.Field(i) + name := field.Name + isExported := ast.IsExported(name) + + if !isExported && !field.Anonymous { + continue + } + + if r.fieldNameMapper != nil { + name = r.fieldNameMapper.FieldName(t, field) + } + + if name != "" && isExported { + if inf, exists := info.Fields[name]; !exists { + info.Names = append(info.Names, name) + } else { + if len(inf.Index) <= len(index) { + continue + } + } + } + + if name != "" || field.Anonymous { + idx := make([]int, len(index)+1) + copy(idx, index) + idx[len(idx)-1] = i + + if name != "" && isExported { + info.Fields[name] = reflectFieldInfo{ + Index: idx, + Anonymous: field.Anonymous, + } + } + if field.Anonymous { + typ := field.Type + for typ.Kind() == reflect.Ptr { + typ = typ.Elem() + } + if typ.Kind() == reflect.Struct { + r.buildFieldInfo(typ, idx, info) + } + } + } + } +} + +var emptyMethodsInfo = reflectMethodsInfo{} + +func (r *Runtime) buildMethodsInfo(t reflect.Type) (info *reflectMethodsInfo) { + n := t.NumMethod() + if n == 0 { + return &emptyMethodsInfo + } + info = new(reflectMethodsInfo) + info.Methods = make(map[string]int, n) + info.Names = make([]string, 0, n) + for i := 0; i < n; i++ { + method := t.Method(i) + name := method.Name + if !ast.IsExported(name) { + continue + } + if r.fieldNameMapper != nil { + name = r.fieldNameMapper.MethodName(t, method) + if name == "" { + continue + } + } + + if _, exists := info.Methods[name]; !exists { + info.Names = append(info.Names, name) + } + + info.Methods[name] = i + } + return +} + +func (r *Runtime) buildFieldsInfo(t reflect.Type) (info *reflectFieldsInfo) { + info = new(reflectFieldsInfo) + n := t.NumField() + info.Fields = make(map[string]reflectFieldInfo, n) + info.Names = make([]string, 0, n) + r.buildFieldInfo(t, nil, info) + return +} + +func (r *Runtime) fieldsInfo(t reflect.Type) (info *reflectFieldsInfo) { + var exists bool + if info, exists = r.fieldsInfoCache[t]; !exists { + info = r.buildFieldsInfo(t) + if r.fieldsInfoCache == nil { + r.fieldsInfoCache = make(map[reflect.Type]*reflectFieldsInfo) + } + r.fieldsInfoCache[t] = info + } + + return +} + +func (r *Runtime) methodsInfo(t reflect.Type) (info *reflectMethodsInfo) { + var exists bool + if info, exists = r.methodsInfoCache[t]; !exists { + info = r.buildMethodsInfo(t) + if r.methodsInfoCache == nil { + r.methodsInfoCache = make(map[reflect.Type]*reflectMethodsInfo) + } + r.methodsInfoCache[t] = info + } + + return +} + +// SetFieldNameMapper sets a custom field name mapper for Go types. It can be called at any time, however +// the mapping for any given value is fixed at the point of creation. +// Setting this to nil restores the default behaviour which is all exported fields and methods are mapped to their +// original unchanged names. +func (r *Runtime) SetFieldNameMapper(mapper FieldNameMapper) { + r.fieldNameMapper = mapper + r.fieldsInfoCache = nil + r.methodsInfoCache = nil +} + +// TagFieldNameMapper returns a FieldNameMapper that uses the given tagName for struct fields and optionally +// uncapitalises (making the first letter lower case) method names. +// The common tag value syntax is supported (name[,options]), however options are ignored. +// Setting name to anything other than a valid ECMAScript identifier makes the field hidden. +func TagFieldNameMapper(tagName string, uncapMethods bool) FieldNameMapper { + return tagFieldNameMapper{ + tagName: tagName, + uncapMethods: uncapMethods, + } +} + +// UncapFieldNameMapper returns a FieldNameMapper that uncapitalises struct field and method names +// making the first letter lower case. +func UncapFieldNameMapper() FieldNameMapper { + return uncapFieldNameMapper{} +} diff --git a/goja/object_goreflect_test.go b/goja/object_goreflect_test.go new file mode 100644 index 0000000..6dc096c --- /dev/null +++ b/goja/object_goreflect_test.go @@ -0,0 +1,1667 @@ +package goja + +import ( + "errors" + "fmt" + "math" + "reflect" + "strings" + "testing" + "time" +) + +func TestGoReflectGet(t *testing.T) { + const SCRIPT = ` + o.X + o.Y; + ` + type O struct { + X int + Y string + } + r := New() + o := O{X: 4, Y: "2"} + r.Set("o", o) + + v, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if s, ok := v.(String); ok { + if s.String() != "42" { + t.Fatalf("Unexpected string: %s", s) + } + } else { + t.Fatalf("Unexpected type: %s", v) + } +} + +func TestGoReflectSet(t *testing.T) { + const SCRIPT = ` + o.X++; + o.Y += "P"; + ` + type O struct { + X int + Y string + } + r := New() + o := O{X: 4, Y: "2"} + r.Set("o", &o) + + _, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if o.X != 5 { + t.Fatalf("Unexpected X: %d", o.X) + } + + if o.Y != "2P" { + t.Fatalf("Unexpected Y: %s", o.Y) + } + + r.Set("o", o) + _, err = r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if res, ok := r.Get("o").Export().(O); ok { + if res.X != 6 { + t.Fatalf("Unexpected res.X: %d", res.X) + } + + if res.Y != "2PP" { + t.Fatalf("Unexpected res.Y: %s", res.Y) + } + } +} + +func TestGoReflectEnumerate(t *testing.T) { + const SCRIPT = ` + var hasX = false; + var hasY = false; + for (var key in o) { + switch (key) { + case "X": + if (hasX) { + throw "Already have X"; + } + hasX = true; + break; + case "Y": + if (hasY) { + throw "Already have Y"; + } + hasY = true; + break; + default: + throw "Unexpected property: " + key; + } + } + hasX && hasY; + ` + + type S struct { + X, Y int + } + + r := New() + r.Set("o", S{X: 40, Y: 2}) + v, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if !v.StrictEquals(valueTrue) { + t.Fatalf("Expected true, got %v", v) + } + +} + +func TestGoReflectCustomIntUnbox(t *testing.T) { + const SCRIPT = ` + i + 2; + ` + + type CustomInt int + var i CustomInt = 40 + + r := New() + r.Set("i", i) + v, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if !v.StrictEquals(intToValue(42)) { + t.Fatalf("Expected int 42, got %v", v) + } +} + +func TestGoReflectPreserveCustomType(t *testing.T) { + const SCRIPT = ` + i; + ` + + type CustomInt int + var i CustomInt = 42 + + r := New() + r.Set("i", i) + v, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + ve := v.Export() + + if ii, ok := ve.(CustomInt); ok { + if ii != i { + t.Fatalf("Wrong value: %v", ii) + } + } else { + t.Fatalf("Wrong type: %v", ve) + } +} + +func TestGoReflectCustomIntValueOf(t *testing.T) { + const SCRIPT = ` + if (i instanceof Number) { + i.valueOf(); + } else { + throw new Error("Value is not a number"); + } + ` + + type CustomInt int + var i CustomInt = 42 + + r := New() + r.Set("i", i) + v, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if !v.StrictEquals(intToValue(42)) { + t.Fatalf("Expected int 42, got %v", v) + } +} + +func TestGoReflectEqual(t *testing.T) { + const SCRIPT = ` + x === y; + ` + + type CustomInt int + var x CustomInt = 42 + var y CustomInt = 42 + + r := New() + r.Set("x", x) + r.Set("y", y) + v, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if !v.StrictEquals(valueTrue) { + t.Fatalf("Expected true, got %v", v) + } +} + +type testGoReflectMethod_O struct { + field string + Test string +} + +func (o testGoReflectMethod_O) Method(s string) string { + return o.field + s +} + +func TestGoReflectMethod(t *testing.T) { + const SCRIPT = ` + o.Method(" 123") + ` + + o := testGoReflectMethod_O{ + field: "test", + } + + r := New() + r.Set("o", &o) + v, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if !v.StrictEquals(asciiString("test 123")) { + t.Fatalf("Expected 'test 123', got %v", v) + } +} + +func (o *testGoReflectMethod_O) Set(s string) { + o.field = s +} + +func (o *testGoReflectMethod_O) Get() string { + return o.field +} + +func TestGoReflectMethodPtr(t *testing.T) { + const SCRIPT = ` + o.Set("42") + o.Get() + ` + + o := testGoReflectMethod_O{ + field: "test", + } + + r := New() + r.Set("o", &o) + v, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if !v.StrictEquals(asciiString("42")) { + t.Fatalf("Expected '42', got %v", v) + } +} + +func (b *testBoolS) Method() bool { + return bool(*b) +} + +func TestGoReflectPtrMethodOnNonPtrValue(t *testing.T) { + var o testGoReflectMethod_O + o.Get() + vm := New() + vm.Set("o", o) + _, err := vm.RunString(`o.Get()`) + if err != nil { + t.Fatal(err) + } + _, err = vm.RunString(`o.Method()`) + if err != nil { + t.Fatal(err) + } + + var b testBoolS + vm.Set("b", b) + _, err = vm.RunString(`b.Method()`) + if err != nil { + t.Fatal(err) + } +} + +func TestGoReflectStructField(t *testing.T) { + type S struct { + F testGoReflectMethod_O + B testBoolS + } + var s S + vm := New() + vm.Set("s", &s) + + const SCRIPT = ` + s.F.Set("Test"); + assert.sameValue(s.F.Method(""), "Test", "1"); + + s.B = true; + assert.sameValue(s.B.Method(), true, "2"); + + assert.sameValue(s.B.toString(), "B", "3"); + ` + + vm.testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestGoReflectProp(t *testing.T) { + const SCRIPT = ` + var d1 = Object.getOwnPropertyDescriptor(o, "Get"); + var d2 = Object.getOwnPropertyDescriptor(o, "Test"); + !d1.writable && !d1.configurable && d2.writable && !d2.configurable; + ` + + o := testGoReflectMethod_O{ + field: "test", + } + + r := New() + r.Set("o", &o) + v, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if !v.StrictEquals(valueTrue) { + t.Fatalf("Expected true, got %v", v) + } +} + +func TestGoReflectRedefineFieldSuccess(t *testing.T) { + const SCRIPT = ` + Object.defineProperty(o, "Test", {value: "AAA"}) === o; + ` + + o := testGoReflectMethod_O{} + + r := New() + r.Set("o", &o) + v, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if !v.StrictEquals(valueTrue) { + t.Fatalf("Expected true, got %v", v) + } + + if o.Test != "AAA" { + t.Fatalf("Expected 'AAA', got '%s'", o.Test) + } + +} + +func TestGoReflectRedefineFieldNonWritable(t *testing.T) { + const SCRIPT = ` + var thrown = false; + try { + Object.defineProperty(o, "Test", {value: "AAA", writable: false}); + } catch (e) { + if (e instanceof TypeError) { + thrown = true; + } else { + throw e; + } + } + thrown; + ` + + o := testGoReflectMethod_O{Test: "Test"} + + r := New() + r.Set("o", &o) + v, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if !v.StrictEquals(valueTrue) { + t.Fatalf("Expected true, got %v", v) + } + + if o.Test != "Test" { + t.Fatalf("Expected 'Test', got: '%s'", o.Test) + } +} + +func TestGoReflectRedefineFieldConfigurable(t *testing.T) { + const SCRIPT = ` + var thrown = false; + try { + Object.defineProperty(o, "Test", {value: "AAA", configurable: true}); + } catch (e) { + if (e instanceof TypeError) { + thrown = true; + } else { + throw e; + } + } + thrown; + ` + + o := testGoReflectMethod_O{Test: "Test"} + + r := New() + r.Set("o", &o) + v, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if !v.StrictEquals(valueTrue) { + t.Fatalf("Expected true, got %v", v) + } + + if o.Test != "Test" { + t.Fatalf("Expected 'Test', got: '%s'", o.Test) + } +} + +func TestGoReflectRedefineMethod(t *testing.T) { + const SCRIPT = ` + var thrown = false; + try { + Object.defineProperty(o, "Method", {value: "AAA", configurable: true}); + } catch (e) { + if (e instanceof TypeError) { + thrown = true; + } else { + throw e; + } + } + thrown; + ` + + o := testGoReflectMethod_O{Test: "Test"} + + r := New() + r.Set("o", &o) + v, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if !v.StrictEquals(valueTrue) { + t.Fatalf("Expected true, got %v", v) + } +} + +func TestGoReflectEmbeddedStruct(t *testing.T) { + const SCRIPT = ` + if (o.ParentField2 !== "ParentField2") { + throw new Error("ParentField2 = " + o.ParentField2); + } + + if (o.Parent.ParentField2 !== 2) { + throw new Error("o.Parent.ParentField2 = " + o.Parent.ParentField2); + } + + if (o.ParentField1 !== 1) { + throw new Error("o.ParentField1 = " + o.ParentField1); + + } + + if (o.ChildField !== 3) { + throw new Error("o.ChildField = " + o.ChildField); + } + + var keys = {}; + for (var k in o) { + if (keys[k]) { + throw new Error("Duplicate key: " + k); + } + keys[k] = true; + } + + var expectedKeys = ["ParentField2", "ParentField1", "Parent", "ChildField"]; + for (var i in expectedKeys) { + if (!keys[expectedKeys[i]]) { + throw new Error("Missing key in enumeration: " + expectedKeys[i]); + } + delete keys[expectedKeys[i]]; + } + + var remainingKeys = Object.keys(keys); + if (remainingKeys.length > 0) { + throw new Error("Unexpected keys: " + remainingKeys); + } + + o.ParentField2 = "ParentField22"; + o.Parent.ParentField2 = 22; + o.ParentField1 = 11; + o.ChildField = 33; + ` + + type Parent struct { + ParentField1 int + ParentField2 int + } + + type Child struct { + ParentField2 string + Parent + ChildField int + } + + vm := New() + o := Child{ + Parent: Parent{ + ParentField1: 1, + ParentField2: 2, + }, + ParentField2: "ParentField2", + ChildField: 3, + } + vm.Set("o", &o) + + _, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if o.ParentField2 != "ParentField22" { + t.Fatalf("ParentField2 = %q", o.ParentField2) + } + + if o.Parent.ParentField2 != 22 { + t.Fatalf("Parent.ParentField2 = %d", o.Parent.ParentField2) + } + + if o.ParentField1 != 11 { + t.Fatalf("ParentField1 = %d", o.ParentField1) + } + + if o.ChildField != 33 { + t.Fatalf("ChildField = %d", o.ChildField) + } +} + +type jsonTagNamer struct{} + +func (jsonTagNamer) FieldName(_ reflect.Type, field reflect.StructField) string { + if jsonTag := field.Tag.Get("json"); jsonTag != "" { + return jsonTag + } + return field.Name +} + +func (jsonTagNamer) MethodName(_ reflect.Type, method reflect.Method) string { + return method.Name +} + +func TestGoReflectCustomNaming(t *testing.T) { + + type testStructWithJsonTags struct { + A string `json:"b"` // <-- script sees field "A" as property "b" + } + + o := &testStructWithJsonTags{"Hello world"} + r := New() + r.SetFieldNameMapper(&jsonTagNamer{}) + r.Set("fn", func() *testStructWithJsonTags { return o }) + + t.Run("get property", func(t *testing.T) { + v, err := r.RunString(`fn().b`) + if err != nil { + t.Fatal(err) + } + if !v.StrictEquals(newStringValue(o.A)) { + t.Fatalf("Expected %q, got %v", o.A, v) + } + }) + + t.Run("set property", func(t *testing.T) { + _, err := r.RunString(`fn().b = "Hello universe"`) + if err != nil { + t.Fatal(err) + } + if o.A != "Hello universe" { + t.Fatalf("Expected \"Hello universe\", got %q", o.A) + } + }) + + t.Run("enumerate properties", func(t *testing.T) { + v, err := r.RunString(`Object.keys(fn())`) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(v.Export(), []interface{}{"b"}) { + t.Fatalf("Expected [\"b\"], got %v", v.Export()) + } + }) +} + +func TestGoReflectCustomObjNaming(t *testing.T) { + + type testStructWithJsonTags struct { + A string `json:"b"` // <-- script sees field "A" as property "b" + } + + r := New() + r.SetFieldNameMapper(&jsonTagNamer{}) + + t.Run("Set object in slice", func(t *testing.T) { + testSlice := &[]testStructWithJsonTags{{"Hello world"}} + r.Set("testslice", testSlice) + _, err := r.RunString(`testslice[0] = {b:"setted"}`) + if err != nil { + t.Fatal(err) + } + if (*testSlice)[0].A != "setted" { + t.Fatalf("Expected \"setted\", got %q", (*testSlice)[0]) + } + }) + + t.Run("Set object in map", func(t *testing.T) { + testMap := map[string]testStructWithJsonTags{"key": {"Hello world"}} + r.Set("testmap", testMap) + _, err := r.RunString(`testmap["key"] = {b:"setted"}`) + if err != nil { + t.Fatal(err) + } + if testMap["key"].A != "setted" { + t.Fatalf("Expected \"setted\", got %q", testMap["key"]) + } + }) + + t.Run("Add object to map", func(t *testing.T) { + testMap := map[string]testStructWithJsonTags{} + r.Set("testmap", testMap) + _, err := r.RunString(`testmap["newkey"] = {b:"setted"}`) + if err != nil { + t.Fatal(err) + } + if testMap["newkey"].A != "setted" { + t.Fatalf("Expected \"setted\", got %q", testMap["newkey"]) + } + }) +} + +type fieldNameMapper1 struct{} + +func (fieldNameMapper1) FieldName(_ reflect.Type, f reflect.StructField) string { + return strings.ToLower(f.Name) +} + +func (fieldNameMapper1) MethodName(_ reflect.Type, m reflect.Method) string { + return m.Name +} + +func TestNonStructAnonFields(t *testing.T) { + type Test1 struct { + M bool + } + type test3 []int + type Test4 []int + type Test2 struct { + test3 + Test4 + *Test1 + } + + const SCRIPT = ` + JSON.stringify(a); + a.m && a.test3 === undefined && a.test4.length === 2 + ` + vm := New() + vm.SetFieldNameMapper(fieldNameMapper1{}) + vm.Set("a", &Test2{Test1: &Test1{M: true}, Test4: []int{1, 2}, test3: nil}) + v, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + if !v.StrictEquals(valueTrue) { + t.Fatalf("Unexepected result: %v", v) + } +} + +func TestStructNonAddressable(t *testing.T) { + type S struct { + Field int + } + + const SCRIPT = ` + "use strict"; + + if (!Object.getOwnPropertyDescriptor(s, "Field").writable) { + throw new Error("s.Field is non-writable"); + } + + if (!Object.getOwnPropertyDescriptor(s1, "Field").writable) { + throw new Error("s1.Field is non-writable"); + } + + s1.Field = 42; + s.Field = 43; + s; +` + + var s S + vm := New() + vm.Set("s", s) + vm.Set("s1", &s) + v, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + exp := v.Export() + if s1, ok := exp.(S); ok { + if s1.Field != 43 { + t.Fatal(s1) + } + } else { + t.Fatalf("Wrong type: %T", exp) + } + if s.Field != 42 { + t.Fatalf("Unexpected s.Field value: %d", s.Field) + } +} + +type testFieldMapper struct { +} + +func (testFieldMapper) FieldName(_ reflect.Type, f reflect.StructField) string { + if tag := f.Tag.Get("js"); tag != "" { + if tag == "-" { + return "" + } + return tag + } + + return f.Name +} + +func (testFieldMapper) MethodName(_ reflect.Type, m reflect.Method) string { + return m.Name +} + +func TestHidingAnonField(t *testing.T) { + type InnerType struct { + AnotherField string + } + + type OuterType struct { + InnerType `js:"-"` + SomeField string + } + + const SCRIPT = ` + var a = Object.getOwnPropertyNames(o); + if (a.length !== 2) { + throw new Error("unexpected length: " + a.length); + } + + if (a.indexOf("SomeField") === -1) { + throw new Error("no SomeField"); + } + + if (a.indexOf("AnotherField") === -1) { + throw new Error("no SomeField"); + } + ` + + var o OuterType + + vm := New() + vm.SetFieldNameMapper(testFieldMapper{}) + vm.Set("o", &o) + + _, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } +} + +func TestFieldOverriding(t *testing.T) { + type InnerType struct { + AnotherField string + AnotherField1 string + } + + type OuterType struct { + InnerType `js:"-"` + SomeField string + AnotherField string `js:"-"` + AnotherField1 string + } + + const SCRIPT = ` + if (o.SomeField !== "SomeField") { + throw new Error("SomeField"); + } + + if (o.AnotherField !== "AnotherField inner") { + throw new Error("AnotherField"); + } + + if (o.AnotherField1 !== "AnotherField1 outer") { + throw new Error("AnotherField1"); + } + + if (o.InnerType) { + throw new Error("InnerType is present"); + } + ` + + o := OuterType{ + InnerType: InnerType{ + AnotherField: "AnotherField inner", + AnotherField1: "AnotherField1 inner", + }, + SomeField: "SomeField", + AnotherField: "AnotherField outer", + AnotherField1: "AnotherField1 outer", + } + + vm := New() + vm.SetFieldNameMapper(testFieldMapper{}) + vm.Set("o", &o) + + _, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } +} + +func TestDefinePropertyUnexportedJsName(t *testing.T) { + type T struct { + Field int + unexported int + } + + vm := New() + vm.SetFieldNameMapper(fieldNameMapper1{}) + vm.Set("f", &T{unexported: 0}) + + _, err := vm.RunString(` + "use strict"; + Object.defineProperty(f, "field", {value: 42}); + if (f.field !== 42) { + throw new Error("Unexpected value: " + f.field); + } + if (f.hasOwnProperty("unexported")) { + throw new Error("hasOwnProperty('unexported') is true"); + } + var thrown; + try { + Object.defineProperty(f, "unexported", {value: 1}); + } catch (e) { + thrown = e; + } + if (!(thrown instanceof TypeError)) { + throw new Error("Unexpected error: ", thrown); + } + `) + if err != nil { + t.Fatal(err) + } +} + +type fieldNameMapperToLower struct{} + +func (fieldNameMapperToLower) FieldName(_ reflect.Type, f reflect.StructField) string { + return strings.ToLower(f.Name) +} + +func (fieldNameMapperToLower) MethodName(_ reflect.Type, m reflect.Method) string { + return strings.ToLower(m.Name) +} + +func TestHasOwnPropertyUnexportedJsName(t *testing.T) { + vm := New() + vm.SetFieldNameMapper(fieldNameMapperToLower{}) + vm.Set("f", &testGoReflectMethod_O{}) + + _, err := vm.RunString(` + "use strict"; + if (!f.hasOwnProperty("test")) { + throw new Error("hasOwnProperty('test') returned false"); + } + if (!f.hasOwnProperty("method")) { + throw new Error("hasOwnProperty('method') returned false"); + } + `) + if err != nil { + t.Fatal(err) + } +} + +func BenchmarkGoReflectGet(b *testing.B) { + type parent struct { + field, Test1, Test2, Test3, Test4, Test5, Test string + } + + type child struct { + parent + Test6 string + } + + b.StopTimer() + vm := New() + + b.StartTimer() + for i := 0; i < b.N; i++ { + v := vm.ToValue(child{parent: parent{Test: "Test", field: ""}}).(*Object) + v.Get("Test") + } +} + +func TestNestedStructSet(t *testing.T) { + type B struct { + Field int + } + type A struct { + B B + } + + const SCRIPT = ` + 'use strict'; + a.B.Field++; + if (a1.B.Field != 1) { + throw new Error("a1.B.Field = " + a1.B.Field); + } + var d = Object.getOwnPropertyDescriptor(a1.B, "Field"); + if (!d.writable) { + throw new Error("a1.B is not writable"); + } + a1.B.Field = 42; + a1; + ` + a := A{ + B: B{ + Field: 1, + }, + } + vm := New() + vm.Set("a", &a) + vm.Set("a1", a) + v, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + exp := v.Export() + if v, ok := exp.(A); ok { + if v.B.Field != 42 { + t.Fatal(v) + } + } else { + t.Fatalf("Wrong type: %T", exp) + } + + if v := a.B.Field; v != 2 { + t.Fatalf("Unexpected a.B.Field: %d", v) + } +} + +func TestStructNonAddressableAnonStruct(t *testing.T) { + + type C struct { + Z int64 + X string + } + + type B struct { + C + Y string + } + + type A struct { + B B + } + + a := A{ + B: B{ + C: C{ + Z: 1, + X: "X2", + }, + Y: "Y3", + }, + } + const SCRIPT = ` + "use strict"; + var s = JSON.stringify(a); + s; +` + + vm := New() + vm.Set("a", &a) + v, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + expected := `{"B":{"C":{"Z":1,"X":"X2"},"Z":1,"X":"X2","Y":"Y3"}}` + if expected != v.String() { + t.Fatalf("Expected '%s', got '%s'", expected, v.String()) + } + +} + +func TestTagFieldNameMapperInvalidId(t *testing.T) { + vm := New() + vm.SetFieldNameMapper(TagFieldNameMapper("json", true)) + type S struct { + Field int `json:"-"` + } + vm.Set("s", S{Field: 42}) + res, err := vm.RunString(`s.hasOwnProperty("field") || s.hasOwnProperty("Field")`) + if err != nil { + t.Fatal(err) + } + if res != valueFalse { + t.Fatalf("Unexpected result: %v", res) + } +} + +func TestPrimitivePtr(t *testing.T) { + vm := New() + s := "test" + vm.Set("s", &s) + res, err := vm.RunString(`s instanceof String && s == "test"`) // note non-strict equality + if err != nil { + t.Fatal(err) + } + if v := res.ToBoolean(); !v { + t.Fatalf("value: %#v", res) + } + s = "test1" + res, err = vm.RunString(`s == "test1"`) + if err != nil { + t.Fatal(err) + } + if v := res.ToBoolean(); !v { + t.Fatalf("value: %#v", res) + } +} + +func TestStringer(t *testing.T) { + vm := New() + vm.Set("e", errors.New("test")) + res, err := vm.RunString("e.toString()") + if err != nil { + t.Fatal(err) + } + if v := res.Export(); v != "test" { + t.Fatalf("v: %v", v) + } +} + +func ExampleTagFieldNameMapper() { + vm := New() + vm.SetFieldNameMapper(TagFieldNameMapper("json", true)) + type S struct { + Field int `json:"field"` + } + vm.Set("s", S{Field: 42}) + res, _ := vm.RunString(`s.field`) + fmt.Println(res.Export()) + // Output: 42 +} + +func ExampleUncapFieldNameMapper() { + vm := New() + s := testGoReflectMethod_O{ + Test: "passed", + } + vm.SetFieldNameMapper(UncapFieldNameMapper()) + vm.Set("s", s) + res, _ := vm.RunString(`s.test + " and " + s.method("passed too")`) + fmt.Println(res.Export()) + // Output: passed and passed too +} + +func TestGoReflectWithProto(t *testing.T) { + type S struct { + Field int + } + var s S + vm := New() + vm.Set("s", &s) + vm.testScriptWithTestLib(` + (function() { + 'use strict'; + var proto = { + Field: "protoField", + test: 42 + }; + var test1Holder; + Object.defineProperty(proto, "test1", { + set: function(v) { + test1Holder = v; + }, + get: function() { + return test1Holder; + } + }); + Object.setPrototypeOf(s, proto); + assert.sameValue(s.Field, 0, "s.Field"); + s.Field = 2; + assert.sameValue(s.Field, 2, "s.Field"); + assert.sameValue(s.test, 42, "s.test"); + assert.throws(TypeError, function() { + Object.defineProperty(s, "test", {value: 43}); + }); + test1Holder = 1; + assert.sameValue(s.test1, 1, "s.test1"); + s.test1 = 2; + assert.sameValue(test1Holder, 2, "test1Holder"); + })(); + `, _undefined, t) +} + +func TestGoReflectSymbols(t *testing.T) { + type S struct { + Field int + } + var s S + vm := New() + vm.Set("s", &s) + _, err := vm.RunString(` + 'use strict'; + var sym = Symbol(66); + s[sym] = "Test"; + if (s[sym] !== "Test") { + throw new Error("s[sym]=" + s[sym]); + } + `) + if err != nil { + t.Fatal(err) + } +} + +func TestGoReflectSymbolEqualityQuirk(t *testing.T) { + type Field struct { + } + type S struct { + Field *Field + } + var s = S{ + Field: &Field{}, + } + vm := New() + vm.Set("s", &s) + res, err := vm.RunString(` + var sym = Symbol(66); + var field1 = s.Field; + field1[sym] = true; + var field2 = s.Field; + // Because a wrapper is created every time the property is accessed + // field1 and field2 will be different instances of the wrapper. + // Symbol properties only exist in the wrapper, they cannot be placed into the original Go value, + // hence the following: + field1 === field2 && field1[sym] === true && field2[sym] === undefined; + `) + if err != nil { + t.Fatal(err) + } + if res != valueTrue { + t.Fatal(res) + } +} + +func TestGoObj__Proto__(t *testing.T) { + type S struct { + Field int + } + vm := New() + vm.Set("s", S{}) + vm.Set("m", map[string]interface{}{}) + vm.Set("mr", map[int]string{}) + vm.Set("a", []interface{}{}) + vm.Set("ar", []string{}) + _, err := vm.RunString(` + function f(s, expectedCtor, prefix) { + if (s.__proto__ !== expectedCtor.prototype) { + throw new Error(prefix + ": __proto__: " + s.__proto__); + } + s.__proto__ = null; + if (s.__proto__ !== undefined) { // as there is no longer a prototype, there is no longer the __proto__ property + throw new Error(prefix + ": __proto__ is not undefined: " + s.__proto__); + } + var proto = Object.getPrototypeOf(s); + if (proto !== null) { + throw new Error(prefix + ": proto is not null: " + proto); + } + } + f(s, Object, "struct"); + f(m, Object, "simple map"); + f(mr, Object, "reflect map"); + f(a, Array, "slice"); + f(ar, Array, "reflect slice"); + `) + if err != nil { + t.Fatal(err) + } +} + +func TestGoReflectUnicodeProps(t *testing.T) { + type S struct { + Тест string + } + vm := New() + var s S + vm.Set("s", &s) + _, err := vm.RunString(` + if (!s.hasOwnProperty("Тест")) { + throw new Error("hasOwnProperty"); + } + `) + if err != nil { + t.Fatal(err) + } +} + +func TestGoReflectPreserveType(t *testing.T) { + vm := New() + var expect = time.Duration(math.MaxInt64) + vm.Set(`make`, func() time.Duration { + return expect + }) + vm.Set(`handle`, func(d time.Duration) { + if d.String() != expect.String() { + t.Fatal(`expect`, expect, `, but get`, d) + } + }) + _, e := vm.RunString(` + var d=make() + handle(d) + `) + if e != nil { + t.Fatal(e) + } +} + +func TestGoReflectCopyOnWrite(t *testing.T) { + type Inner struct { + Field int + } + type S struct { + I Inner + } + var s S + s.I.Field = 1 + + vm := New() + vm.Set("s", &s) + _, err := vm.RunString(` + if (s.I.Field !== 1) { + throw new Error("s.I.Field: " + s.I.Field); + } + + let tmp = s.I; // tmp becomes a reference to s.I + if (tmp.Field !== 1) { + throw new Error("tmp.Field: " + tmp.Field); + } + + s.I.Field = 2; + if (s.I.Field !== 2) { + throw new Error("s.I.Field (1): " + s.I.Field); + } + if (tmp.Field !== 2) { + throw new Error("tmp.Field (1): " + tmp.Field); + } + + s.I = {Field: 3}; // at this point tmp is changed to a copy + if (s.I.Field !== 3) { + throw new Error("s.I.Field (2): " + s.I.Field); + } + if (tmp.Field !== 2) { + throw new Error("tmp.Field (2): " + tmp.Field); + } + `) + + if err != nil { + t.Fatal(err) + } +} + +func TestReflectSetReflectValue(t *testing.T) { + o := []testGoReflectMethod_O{{}} + vm := New() + vm.Set("o", o) + _, err := vm.RunString(` + const t = o[0]; + t.Set("a"); + o[0] = {}; + o[0].Set("b"); + if (t.Get() !== "a") { + throw new Error(); + } + `) + + if err != nil { + t.Fatal(err) + } +} + +func TestReflectOverwriteReflectMap(t *testing.T) { + vm := New() + type S struct { + M map[int]interface{} + } + var s S + s.M = map[int]interface{}{ + 0: true, + } + vm.Set("s", &s) + _, err := vm.RunString(` + s.M = {1: false}; + `) + if err != nil { + t.Fatal(err) + } + if _, exists := s.M[0]; exists { + t.Fatal(s) + } +} + +type testBoolS bool + +func (testBoolS) String() string { + return "B" +} + +type testIntS int + +func (testIntS) String() string { + return "I" +} + +type testStringS string + +func (testStringS) String() string { + return "S" +} + +func TestGoReflectToPrimitive(t *testing.T) { + vm := New() + + f := func(expr string, expected Value, t *testing.T) { + v, err := vm.RunString(expr) + if err != nil { + t.Fatal(err) + } + if IsNaN(expected) { + if IsNaN(v) { + return + } + } else { + if v.StrictEquals(expected) { + return + } + } + t.Fatalf("%s: expected: %v, actual: %v", expr, expected, v) + } + + t.Run("Not Stringers", func(t *testing.T) { + type Bool bool + var b Bool = true + + t.Run("Bool", func(t *testing.T) { + vm.Set("b", b) + f("+b", intToValue(1), t) + f("`${b}`", asciiString("true"), t) + f("b.toString()", asciiString("true"), t) + f("b.valueOf()", valueTrue, t) + }) + + t.Run("*Bool", func(t *testing.T) { + vm.Set("b", &b) + f("+b", intToValue(1), t) + f("`${b}`", asciiString("true"), t) + f("b.toString()", asciiString("true"), t) + f("b.valueOf()", valueTrue, t) + }) + + type Int int + var i Int = 1 + + t.Run("Int", func(t *testing.T) { + vm.Set("i", i) + f("+i", intToValue(1), t) + f("`${i}`", asciiString("1"), t) + f("i.toString()", asciiString("1"), t) + f("i.valueOf()", intToValue(1), t) + }) + + t.Run("*Int", func(t *testing.T) { + vm.Set("i", &i) + f("+i", intToValue(1), t) + f("`${i}`", asciiString("1"), t) + f("i.toString()", asciiString("1"), t) + f("i.valueOf()", intToValue(1), t) + }) + + type Uint uint + var ui Uint = 1 + + t.Run("Uint", func(t *testing.T) { + vm.Set("ui", ui) + f("+ui", intToValue(1), t) + f("`${ui}`", asciiString("1"), t) + f("ui.toString()", asciiString("1"), t) + f("ui.valueOf()", intToValue(1), t) + }) + + t.Run("*Uint", func(t *testing.T) { + vm.Set("ui", &i) + f("+ui", intToValue(1), t) + f("`${ui}`", asciiString("1"), t) + f("ui.toString()", asciiString("1"), t) + f("ui.valueOf()", intToValue(1), t) + }) + + type Float float64 + var fl Float = 1.1 + + t.Run("Float", func(t *testing.T) { + vm.Set("fl", fl) + f("+fl", floatToValue(1.1), t) + f("`${fl}`", asciiString("1.1"), t) + f("fl.toString()", asciiString("1.1"), t) + f("fl.valueOf()", floatToValue(1.1), t) + }) + + t.Run("*Float", func(t *testing.T) { + vm.Set("fl", &fl) + f("+fl", floatToValue(1.1), t) + f("`${fl}`", asciiString("1.1"), t) + f("fl.toString()", asciiString("1.1"), t) + f("fl.valueOf()", floatToValue(1.1), t) + }) + + fl = Float(math.Inf(1)) + t.Run("FloatInf", func(t *testing.T) { + vm.Set("fl", fl) + f("+fl", _positiveInf, t) + f("fl.toString()", asciiString("Infinity"), t) + }) + + type Empty struct{} + + var e Empty + t.Run("Empty", func(t *testing.T) { + vm.Set("e", &e) + f("+e", _NaN, t) + f("`${e}`", asciiString("[object Object]"), t) + f("e.toString()", asciiString("[object Object]"), t) + f("e.valueOf()", vm.ToValue(&e), t) + }) + }) + + t.Run("Stringers", func(t *testing.T) { + var b testBoolS = true + t.Run("Bool", func(t *testing.T) { + vm.Set("b", b) + f("`${b}`", asciiString("B"), t) + f("b.toString()", asciiString("B"), t) + f("b.valueOf()", valueTrue, t) + f("+b", intToValue(1), t) + }) + + t.Run("*Bool", func(t *testing.T) { + vm.Set("b", &b) + f("`${b}`", asciiString("B"), t) + f("b.toString()", asciiString("B"), t) + f("b.valueOf()", valueTrue, t) + f("+b", intToValue(1), t) + }) + + var i testIntS = 1 + t.Run("Int", func(t *testing.T) { + vm.Set("i", i) + f("`${i}`", asciiString("I"), t) + f("i.toString()", asciiString("I"), t) + f("i.valueOf()", intToValue(1), t) + f("+i", intToValue(1), t) + }) + + t.Run("*Int", func(t *testing.T) { + vm.Set("i", &i) + f("`${i}`", asciiString("I"), t) + f("i.toString()", asciiString("I"), t) + f("i.valueOf()", intToValue(1), t) + f("+i", intToValue(1), t) + }) + + var s testStringS + t.Run("String", func(t *testing.T) { + vm.Set("s", s) + f("`${s}`", asciiString("S"), t) + f("s.toString()", asciiString("S"), t) + f("s.valueOf()", asciiString("S"), t) + f("+s", _NaN, t) + }) + + t.Run("*String", func(t *testing.T) { + vm.Set("s", &s) + f("`${s}`", asciiString("S"), t) + f("s.toString()", asciiString("S"), t) + f("s.valueOf()", asciiString("S"), t) + f("+s", _NaN, t) + }) + }) +} + +type testGoReflectFuncRt struct { +} + +func (*testGoReflectFuncRt) M(call FunctionCall, r *Runtime) Value { + if r == nil { + panic(typeError("Runtime is nil")) + } + return call.Argument(0) +} + +func (*testGoReflectFuncRt) C(call ConstructorCall, r *Runtime) *Object { + if r == nil { + panic(typeError("Runtime is nil in constructor")) + } + call.This.Set("r", call.Argument(0)) + return nil +} + +func TestGoReflectFuncWithRuntime(t *testing.T) { + vm := New() + var s testGoReflectFuncRt + vm.Set("s", &s) + res, err := vm.RunString("s.M(true)") + if err != nil { + t.Fatal(err) + } + if res != valueTrue { + t.Fatal(res) + } + + res, err = vm.RunString("new s.C(true).r") + if err != nil { + t.Fatal(err) + } + if res != valueTrue { + t.Fatal(res) + } +} + +func TestGoReflectDefaultToString(t *testing.T) { + var s testStringS + vm := New() + v := vm.ToValue(s).(*Object) + v.Delete("toString") + v.Delete("valueOf") + vm.Set("s", v) + _, err := vm.RunString(` + class S { + toString() { + return "X"; + } + } + + if (s.toString() !== "S") { + throw new Error(s.toString()); + } + if (("" + s) !== "S") { + throw new Error("" + s); + } + + Object.setPrototypeOf(s, S.prototype); + if (s.toString() !== "X") { + throw new Error(s.toString()); + } + if (("" + s) !== "X") { + throw new Error("" + s); + } + `) + if err != nil { + t.Fatal(err) + } +} + +func TestGoReflectUnexportedEmbedStruct(t *testing.T) { + type privateEmbed struct { + A string + } + type PublicEmbed struct { + B string + } + type privateNested struct { + C string + } + type PublicNested struct { + D string + } + type Foo struct { + privateEmbed + PublicEmbed + + privateNested privateNested + PublicNested PublicNested + + e string + F string + } + + vm := New() + vm.Set("foo", Foo{ + privateEmbed: privateEmbed{A: "testA"}, + PublicEmbed: PublicEmbed{B: "testB"}, + privateNested: privateNested{C: "testC"}, + PublicNested: PublicNested{D: "testD"}, + e: "testE", + F: "testF", + }) + + scenarios := []struct { + expr string + expected string + }{ + {"foo.privateEmbed", "undefined"}, + {"foo.A", "testA"}, + // --- + {"foo.PublicEmbed", "[object Object]"}, + {"foo.B", "testB"}, + {"foo.PublicEmbed.B", "testB"}, + // --- + {"foo.privateNested", "undefined"}, + {"foo.C", "undefined"}, + // --- + {"foo.PublicNested", "[object Object]"}, + {"foo.D", "undefined"}, + {"foo.PublicNested.D", "testD"}, + // --- + {"foo.e", "undefined"}, + {"foo.F", "testF"}, + } + + for _, s := range scenarios { + t.Run(s.expr, func(t *testing.T) { + v, err := vm.RunString(s.expr) + if err != nil { + t.Fatal(err) + } + + vStr := v.String() + + if vStr != s.expected { + t.Fatalf("Expected %q, got %q", s.expected, vStr) + } + }) + } +} diff --git a/goja/object_goslice.go b/goja/object_goslice.go new file mode 100644 index 0000000..1a52207 --- /dev/null +++ b/goja/object_goslice.go @@ -0,0 +1,343 @@ +package goja + +import ( + "math" + "math/bits" + "reflect" + "strconv" + + "github.com/dop251/goja/unistring" +) + +type objectGoSlice struct { + baseObject + data *[]interface{} + lengthProp valueProperty + origIsPtr bool +} + +func (r *Runtime) newObjectGoSlice(data *[]interface{}, isPtr bool) *objectGoSlice { + obj := &Object{runtime: r} + a := &objectGoSlice{ + baseObject: baseObject{ + val: obj, + }, + data: data, + origIsPtr: isPtr, + } + obj.self = a + a.init() + + return a +} + +func (o *objectGoSlice) init() { + o.baseObject.init() + o.class = classArray + o.prototype = o.val.runtime.getArrayPrototype() + o.lengthProp.writable = true + o.extensible = true + o.baseObject._put("length", &o.lengthProp) +} + +func (o *objectGoSlice) updateLen() { + o.lengthProp.value = intToValue(int64(len(*o.data))) +} + +func (o *objectGoSlice) _getIdx(idx int) Value { + return o.val.runtime.ToValue((*o.data)[idx]) +} + +func (o *objectGoSlice) getStr(name unistring.String, receiver Value) Value { + var ownProp Value + if idx := strToGoIdx(name); idx >= 0 && idx < len(*o.data) { + ownProp = o._getIdx(idx) + } else if name == "length" { + o.updateLen() + ownProp = &o.lengthProp + } + + return o.getStrWithOwnProp(ownProp, name, receiver) +} + +func (o *objectGoSlice) getIdx(idx valueInt, receiver Value) Value { + if idx := int64(idx); idx >= 0 && idx < int64(len(*o.data)) { + return o._getIdx(int(idx)) + } + if o.prototype != nil { + if receiver == nil { + return o.prototype.self.getIdx(idx, o.val) + } + return o.prototype.self.getIdx(idx, receiver) + } + return nil +} + +func (o *objectGoSlice) getOwnPropStr(name unistring.String) Value { + if idx := strToGoIdx(name); idx >= 0 { + if idx < len(*o.data) { + return &valueProperty{ + value: o._getIdx(idx), + writable: true, + enumerable: true, + } + } + return nil + } + if name == "length" { + o.updateLen() + return &o.lengthProp + } + return nil +} + +func (o *objectGoSlice) getOwnPropIdx(idx valueInt) Value { + if idx := int64(idx); idx >= 0 && idx < int64(len(*o.data)) { + return &valueProperty{ + value: o._getIdx(int(idx)), + writable: true, + enumerable: true, + } + } + return nil +} + +func (o *objectGoSlice) grow(size int) { + oldcap := cap(*o.data) + if oldcap < size { + n := make([]interface{}, size, growCap(size, len(*o.data), oldcap)) + copy(n, *o.data) + *o.data = n + } else { + tail := (*o.data)[len(*o.data):size] + for k := range tail { + tail[k] = nil + } + *o.data = (*o.data)[:size] + } +} + +func (o *objectGoSlice) shrink(size int) { + tail := (*o.data)[size:] + for k := range tail { + tail[k] = nil + } + *o.data = (*o.data)[:size] +} + +func (o *objectGoSlice) putIdx(idx int, v Value, throw bool) { + if idx >= len(*o.data) { + o.grow(idx + 1) + } + (*o.data)[idx] = v.Export() +} + +func (o *objectGoSlice) putLength(v uint32, throw bool) bool { + if bits.UintSize == 32 && v > math.MaxInt32 { + panic(rangeError("Integer value overflows 32-bit int")) + } + newLen := int(v) + curLen := len(*o.data) + if newLen > curLen { + o.grow(newLen) + } else if newLen < curLen { + o.shrink(newLen) + } + return true +} + +func (o *objectGoSlice) setOwnIdx(idx valueInt, val Value, throw bool) bool { + if i := toIntStrict(int64(idx)); i >= 0 { + if i >= len(*o.data) { + if res, ok := o._setForeignIdx(idx, nil, val, o.val, throw); ok { + return res + } + } + o.putIdx(i, val, throw) + } else { + name := idx.string() + if res, ok := o._setForeignStr(name, nil, val, o.val, throw); !ok { + o.val.runtime.typeErrorResult(throw, "Can't set property '%s' on Go slice", name) + return false + } else { + return res + } + } + return true +} + +func (o *objectGoSlice) setOwnStr(name unistring.String, val Value, throw bool) bool { + if idx := strToGoIdx(name); idx >= 0 { + if idx >= len(*o.data) { + if res, ok := o._setForeignStr(name, nil, val, o.val, throw); ok { + return res + } + } + o.putIdx(idx, val, throw) + } else { + if name == "length" { + return o.putLength(o.val.runtime.toLengthUint32(val), throw) + } + if res, ok := o._setForeignStr(name, nil, val, o.val, throw); !ok { + o.val.runtime.typeErrorResult(throw, "Can't set property '%s' on Go slice", name) + return false + } else { + return res + } + } + return true +} + +func (o *objectGoSlice) setForeignIdx(idx valueInt, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignIdx(idx, trueValIfPresent(o.hasOwnPropertyIdx(idx)), val, receiver, throw) +} + +func (o *objectGoSlice) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignStr(name, trueValIfPresent(o.hasOwnPropertyStr(name)), val, receiver, throw) +} + +func (o *objectGoSlice) hasOwnPropertyIdx(idx valueInt) bool { + if idx := int64(idx); idx >= 0 { + return idx < int64(len(*o.data)) + } + return false +} + +func (o *objectGoSlice) hasOwnPropertyStr(name unistring.String) bool { + if idx := strToIdx64(name); idx >= 0 { + return idx < int64(len(*o.data)) + } + return name == "length" +} + +func (o *objectGoSlice) defineOwnPropertyIdx(idx valueInt, descr PropertyDescriptor, throw bool) bool { + if i := toIntStrict(int64(idx)); i >= 0 { + if !o.val.runtime.checkHostObjectPropertyDescr(idx.string(), descr, throw) { + return false + } + val := descr.Value + if val == nil { + val = _undefined + } + o.putIdx(i, val, throw) + return true + } + o.val.runtime.typeErrorResult(throw, "Cannot define property '%d' on a Go slice", idx) + return false +} + +func (o *objectGoSlice) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + if idx := strToGoIdx(name); idx >= 0 { + if !o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) { + return false + } + val := descr.Value + if val == nil { + val = _undefined + } + o.putIdx(idx, val, throw) + return true + } + if name == "length" { + return o.val.runtime.defineArrayLength(&o.lengthProp, descr, o.putLength, throw) + } + o.val.runtime.typeErrorResult(throw, "Cannot define property '%s' on a Go slice", name) + return false +} + +func (o *objectGoSlice) _deleteIdx(idx int64) { + if idx < int64(len(*o.data)) { + (*o.data)[idx] = nil + } +} + +func (o *objectGoSlice) deleteStr(name unistring.String, throw bool) bool { + if idx := strToIdx64(name); idx >= 0 { + o._deleteIdx(idx) + return true + } + return o.baseObject.deleteStr(name, throw) +} + +func (o *objectGoSlice) deleteIdx(i valueInt, throw bool) bool { + idx := int64(i) + if idx >= 0 { + o._deleteIdx(idx) + } + return true +} + +type goslicePropIter struct { + o *objectGoSlice + idx, limit int +} + +func (i *goslicePropIter) next() (propIterItem, iterNextFunc) { + if i.idx < i.limit && i.idx < len(*i.o.data) { + name := strconv.Itoa(i.idx) + i.idx++ + return propIterItem{name: newStringValue(name), enumerable: _ENUM_TRUE}, i.next + } + + return propIterItem{}, nil +} + +func (o *objectGoSlice) iterateStringKeys() iterNextFunc { + return (&goslicePropIter{ + o: o, + limit: len(*o.data), + }).next +} + +func (o *objectGoSlice) stringKeys(_ bool, accum []Value) []Value { + for i := range *o.data { + accum = append(accum, asciiString(strconv.Itoa(i))) + } + + return accum +} + +func (o *objectGoSlice) export(*objectExportCtx) interface{} { + if o.origIsPtr { + return o.data + } + return *o.data +} + +func (o *objectGoSlice) exportType() reflect.Type { + if o.origIsPtr { + return reflectTypeArrayPtr + } + return reflectTypeArray +} + +func (o *objectGoSlice) equal(other objectImpl) bool { + if other, ok := other.(*objectGoSlice); ok { + return o.data == other.data + } + return false +} + +func (o *objectGoSlice) esValue() Value { + return o.val +} + +func (o *objectGoSlice) reflectValue() reflect.Value { + return reflect.ValueOf(o.data).Elem() +} + +func (o *objectGoSlice) setReflectValue(value reflect.Value) { + o.data = value.Addr().Interface().(*[]interface{}) +} + +func (o *objectGoSlice) sortLen() int { + return len(*o.data) +} + +func (o *objectGoSlice) sortGet(i int) Value { + return o.getIdx(valueInt(i), nil) +} + +func (o *objectGoSlice) swap(i int, j int) { + (*o.data)[i], (*o.data)[j] = (*o.data)[j], (*o.data)[i] +} diff --git a/goja/object_goslice_reflect.go b/goja/object_goslice_reflect.go new file mode 100644 index 0000000..4c28d8c --- /dev/null +++ b/goja/object_goslice_reflect.go @@ -0,0 +1,89 @@ +package goja + +import ( + "math" + "math/bits" + "reflect" + + "github.com/dop251/goja/unistring" +) + +type objectGoSliceReflect struct { + objectGoArrayReflect +} + +func (o *objectGoSliceReflect) init() { + o.objectGoArrayReflect._init() + o.lengthProp.writable = true + o.putIdx = o._putIdx +} + +func (o *objectGoSliceReflect) _putIdx(idx int, v Value, throw bool) bool { + if idx >= o.fieldsValue.Len() { + o.grow(idx + 1) + } + return o.objectGoArrayReflect._putIdx(idx, v, throw) +} + +func (o *objectGoSliceReflect) grow(size int) { + oldcap := o.fieldsValue.Cap() + if oldcap < size { + n := reflect.MakeSlice(o.fieldsValue.Type(), size, growCap(size, o.fieldsValue.Len(), oldcap)) + reflect.Copy(n, o.fieldsValue) + o.fieldsValue.Set(n) + l := len(o.valueCache) + if l > size { + l = size + } + for i, w := range o.valueCache[:l] { + if w != nil { + w.setReflectValue(o.fieldsValue.Index(i)) + } + } + } else { + tail := o.fieldsValue.Slice(o.fieldsValue.Len(), size) + zero := reflect.Zero(o.fieldsValue.Type().Elem()) + for i := 0; i < tail.Len(); i++ { + tail.Index(i).Set(zero) + } + o.fieldsValue.SetLen(size) + } +} + +func (o *objectGoSliceReflect) shrink(size int) { + o.valueCache.shrink(size) + tail := o.fieldsValue.Slice(size, o.fieldsValue.Len()) + zero := reflect.Zero(o.fieldsValue.Type().Elem()) + for i := 0; i < tail.Len(); i++ { + tail.Index(i).Set(zero) + } + o.fieldsValue.SetLen(size) +} + +func (o *objectGoSliceReflect) putLength(v uint32, throw bool) bool { + if bits.UintSize == 32 && v > math.MaxInt32 { + panic(rangeError("Integer value overflows 32-bit int")) + } + newLen := int(v) + curLen := o.fieldsValue.Len() + if newLen > curLen { + o.grow(newLen) + } else if newLen < curLen { + o.shrink(newLen) + } + return true +} + +func (o *objectGoSliceReflect) setOwnStr(name unistring.String, val Value, throw bool) bool { + if name == "length" { + return o.putLength(o.val.runtime.toLengthUint32(val), throw) + } + return o.objectGoArrayReflect.setOwnStr(name, val, throw) +} + +func (o *objectGoSliceReflect) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + if name == "length" { + return o.val.runtime.defineArrayLength(&o.lengthProp, descr, o.putLength, throw) + } + return o.objectGoArrayReflect.defineOwnPropertyStr(name, descr, throw) +} diff --git a/goja/object_goslice_reflect_test.go b/goja/object_goslice_reflect_test.go new file mode 100644 index 0000000..c6137c3 --- /dev/null +++ b/goja/object_goslice_reflect_test.go @@ -0,0 +1,520 @@ +package goja + +import ( + "reflect" + "testing" +) + +func TestGoSliceReflectBasic(t *testing.T) { + const SCRIPT = ` + var sum = 0; + for (var i = 0; i < a.length; i++) { + sum += a[i]; + } + sum; + ` + r := New() + r.Set("a", []int{1, 2, 3, 4}) + v, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + if i := v.ToInteger(); i != 10 { + t.Fatalf("Expected 10, got: %d", i) + } + +} + +func TestGoSliceReflectIn(t *testing.T) { + const SCRIPT = ` + var idx = ""; + for (var i in a) { + idx += i; + } + idx; + ` + r := New() + r.Set("a", []int{1, 2, 3, 4}) + v, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + if i := v.String(); i != "0123" { + t.Fatalf("Expected '0123', got: '%s'", i) + } +} + +func TestGoSliceReflectSet(t *testing.T) { + const SCRIPT = ` + a[0] = 33; + a[1] = 333; + a[2] = "42"; + a[3] = {}; + a[4] = 0; + ` + r := New() + a := []int8{1, 2, 3, 4} + r.Set("a", a) + _, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if a[0] != 33 { + t.Fatalf("a[0] = %d, expected 33", a[0]) + } + if a[1] != 77 { + t.Fatalf("a[1] = %d, expected 77", a[1]) + } + if a[2] != 42 { + t.Fatalf("a[2] = %d, expected 42", a[2]) + } + if a[3] != 0 { + t.Fatalf("a[3] = %d, expected 0", a[3]) + } +} + +func TestGoSliceReflectPush(t *testing.T) { + + r := New() + + t.Run("Can push to array by array ptr", func(t *testing.T) { + a := []int8{1} + r.Set("a", &a) + _, err := r.RunString(`a.push (10)`) + if err != nil { + t.Fatal(err) + } + + if a[1] != 10 { + t.Fatalf("a[1] = %d, expected 10", a[1]) + } + }) + + t.Run("Can push to array by struct ptr", func(t *testing.T) { + type testStr struct { + A []int + } + a := testStr{ + A: []int{2}, + } + + r.Set("a", &a) + _, err := r.RunString(`a.A.push (10)`) + if err != nil { + t.Fatal(err) + } + + if a.A[1] != 10 { + t.Fatalf("a[1] = %v, expected 10", a) + } + }) + +} + +func TestGoSliceReflectStructField(t *testing.T) { + vm := New() + var s struct { + A []int + B *[]int + } + vm.Set("s", &s) + _, err := vm.RunString(` + 'use strict'; + s.A.push(1); + if (s.B !== null) { + throw new Error("s.B is not null: " + s.B); + } + s.B = [2]; + `) + if err != nil { + t.Fatal(err) + } + if len(s.A) != 1 || s.A[0] != 1 { + t.Fatalf("s.A: %v", s.A) + } + if len(*s.B) != 1 || (*s.B)[0] != 2 { + t.Fatalf("s.B: %v", *s.B) + } +} + +func TestGoSliceReflectExportToStructField(t *testing.T) { + vm := New() + v, err := vm.RunString(`({A: [1], B: [2]})`) + if err != nil { + t.Fatal(err) + } + var s struct { + A []int + B *[]int + } + err = vm.ExportTo(v, &s) + if err != nil { + t.Fatal(err) + } + if len(s.A) != 1 || s.A[0] != 1 { + t.Fatalf("s.A: %v", s.A) + } + if len(*s.B) != 1 || (*s.B)[0] != 2 { + t.Fatalf("s.B: %v", *s.B) + } +} + +func TestGoSliceReflectProtoMethod(t *testing.T) { + const SCRIPT = ` + a.join(",") + ` + + r := New() + a := []int8{1, 2, 3, 4} + r.Set("a", a) + ret, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + if s := ret.String(); s != "1,2,3,4" { + t.Fatalf("Unexpected result: '%s'", s) + } +} + +type gosliceReflect_withMethods []interface{} + +func (s gosliceReflect_withMethods) Method() bool { + return true +} + +func TestGoSliceReflectMethod(t *testing.T) { + const SCRIPT = ` + typeof a === "object" && a[0] === 42 && a.Method() === true; + ` + + vm := New() + a := make(gosliceReflect_withMethods, 1) + a[0] = 42 + vm.Set("a", a) + v, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + if !v.StrictEquals(valueTrue) { + t.Fatalf("Expected true, got %v", v) + } + +} + +func TestGoSliceReflectGetStr(t *testing.T) { + r := New() + v := r.ToValue([]string{"test"}) + if o, ok := v.(*Object); ok { + if e := o.Get("0").Export(); e != "test" { + t.Fatalf("Unexpected o.Get(\"0\"): %v", e) + } + } +} + +func TestGoSliceReflectNilObjectIfaceVal(t *testing.T) { + r := New() + a := []Value{(*Object)(nil)} + r.Set("a", a) + ret, err := r.RunString(` + ""+a[0]; + `) + if err != nil { + t.Fatal(err) + } + if !asciiString("null").SameAs(ret) { + t.Fatalf("ret: %v", ret) + } +} + +func TestGoSliceReflectSetLength(t *testing.T) { + r := New() + a := []int{1, 2, 3, 4} + b := []testing.TB{&testing.T{}, &testing.T{}, (*testing.T)(nil)} + r.Set("a", &a) + r.Set("b", &b) + _, err := r.RunString(` + 'use strict'; + a.length = 3; + if (a.length !== 3) { + throw new Error("length="+a.length); + } + if (a[3] !== undefined) { + throw new Error("a[3]="+a[3]); + } + a.length = 5; + if (a.length !== 5) { + throw new Error("a.length="+a.length); + } + if (a[3] !== 0) { + throw new Error("a[3]="+a[3]); + } + if (a[4] !== 0) { + throw new Error("a[4]="+a[4]); + } + + b.length = 3; + if (b.length !== 3) { + throw new Error("b.length="+b.length); + } + if (b[3] !== undefined) { + throw new Error("b[3]="+b[3]); + } + b.length = 5; + if (b.length !== 5) { + throw new Error("length="+b.length); + } + if (b[3] !== null) { + throw new Error("b[3]="+b[3]); + } + if (b[4] !== null) { + throw new Error("b[4]="+b[4]); + } + if (b[2] !== null) { + throw new Error("b[2]="+b[2]); + } + `) + if err != nil { + t.Fatal(err) + } +} + +func TestGoSliceReflectProto(t *testing.T) { + r := New() + a := []*Object{{}, nil, {}} + r.Set("a", &a) + r.testScriptWithTestLib(` + var proto = [,2,,4]; + Object.setPrototypeOf(a, proto); + assert.sameValue(a[1], null, "a[1]"); + assert.sameValue(a[3], 4, "a[3]"); + var desc = Object.getOwnPropertyDescriptor(a, "1"); + assert.sameValue(desc.value, null, "desc.value"); + assert(desc.writable, "writable"); + assert(desc.enumerable, "enumerable"); + assert(!desc.configurable, "configurable"); + var v5; + Object.defineProperty(proto, "5", { + set: function(v) { + v5 = v; + } + }); + a[5] = "test"; + assert.sameValue(v5, "test", "v5"); + `, _undefined, t) +} + +func TestGoSliceReflectProtoProto(t *testing.T) { + r := New() + a := []*Object{{}, nil, {}} + proto := []*Object{{}, {}, {}, {}} + r.Set("a", &a) + r.Set("proto", proto) + _, err := r.RunString(` + "use strict"; + var protoproto = {}; + Object.defineProperty(protoproto, "3", { + value: 42 + }); + Object.setPrototypeOf(proto, protoproto); + Object.setPrototypeOf(a, proto); + if (a.hasOwnProperty("3")) { + throw new Error("a.hasOwnProperty(\"3\")"); + } + if (a[3] !== null) { + throw new Error("a[3]="+a[3]); + } + a[3] = null; + if (a[3] !== null) { + throw new Error("a[3]=" + a[3]); + } + `) + if err != nil { + t.Fatal(err) + } + +} + +func TestGoSliceReflectDelete(t *testing.T) { + r := New() + a := []*Object{{}, nil, {}} + r.Set("a", a) + v, err := r.RunString(` + delete a[0] && delete a[1] && delete a[3]; + `) + if err != nil { + t.Fatal(err) + } + if v != valueTrue { + t.Fatalf("not true: %v", v) + } +} + +func TestGoSliceReflectPop(t *testing.T) { + r := New() + a := []string{"1", "", "3"} + r.Set("a", &a) + v, err := r.RunString(` + a.pop() + `) + if err != nil { + t.Fatal(err) + } + if !v.SameAs(asciiString("3")) { + t.Fatal(v) + } +} + +func TestGoSliceReflectPopNoPtr(t *testing.T) { + r := New() + a := []string{"1", "", "3"} + r.Set("a", a) + v, err := r.RunString(` + a.pop() + `) + if err != nil { + t.Fatal(err) + } + if !v.SameAs(asciiString("3")) { + t.Fatal(v) + } +} + +func TestGoSliceReflectLengthProperty(t *testing.T) { + vm := New() + vm.Set("s", []int{2, 3, 4}) + _, err := vm.RunString(` + if (!s.hasOwnProperty("length")) { + throw new Error("hasOwnProperty() returned false"); + } + let desc = Object.getOwnPropertyDescriptor(s, "length"); + if (desc.value !== 3 || !desc.writable || desc.enumerable || desc.configurable) { + throw new Error("incorrect property descriptor: " + JSON.stringify(desc)); + } + `) + if err != nil { + t.Fatal(err) + } +} + +type testCustomSliceWithMethods []int + +func (a testCustomSliceWithMethods) Method() bool { + return true +} + +func TestGoSliceReflectMethods(t *testing.T) { + vm := New() + vm.Set("s", testCustomSliceWithMethods{1, 2, 3}) + _, err := vm.RunString(` + if (!s.hasOwnProperty("Method")) { + throw new Error("hasOwnProperty() returned false"); + } + let desc = Object.getOwnPropertyDescriptor(s, "Method"); + if (desc.value() !== true || desc.writable || !desc.enumerable || desc.configurable) { + throw new Error("incorrect property descriptor: " + JSON.stringify(desc)); + } + `) + if err != nil { + t.Fatal(err) + } +} + +func TestGoSliceReflectExportAfterGrow(t *testing.T) { + vm := New() + vm.Set("a", []int{1}) + v, err := vm.RunString(` + a.push(2); + a; + `) + if err != nil { + t.Fatal(err) + } + exp := v.Export() + if a, ok := exp.([]int); ok { + if len(a) != 2 || a[0] != 1 || a[1] != 2 { + t.Fatal(a) + } + } else { + t.Fatalf("Wrong type: %T", exp) + } +} + +func TestGoSliceReflectSort(t *testing.T) { + vm := New() + type Thing struct{ Name string } + vm.Set("v", []*Thing{ + {Name: "log"}, + {Name: "etc"}, + {Name: "test"}, + {Name: "bin"}, + }) + ret, err := vm.RunString(` +//v.sort((a, b) => a.Name.localeCompare(b.Name)).map((x) => x.Name); + const tmp = v[0]; + v[0] = v[1]; + v[1] = tmp; + v[0].Name + v[1].Name; +`) + if err != nil { + panic(err) + } + t.Log(ret.Export()) +} + +func TestGoSliceReflect111(t *testing.T) { + vm := New() + vm.Set("v", []int32{ + 1, 2, + }) + ret, err := vm.RunString(` +//v.sort((a, b) => a.Name.localeCompare(b.Name)).map((x) => x.Name); + const tmp = v[0]; + v[0] = v[1]; + v[1] = tmp; + "" + v[0] + v[1]; +`) + if err != nil { + panic(err) + } + t.Log(ret.Export()) + a := []int{1, 2} + a0 := reflect.ValueOf(a).Index(0) + a0.Set(reflect.ValueOf(0)) + t.Log(a[0]) +} + +func TestGoSliceReflectExternalLenUpdate(t *testing.T) { + data := &[]int{1} + + vm := New() + vm.Set("data", data) + vm.Set("append", func(a *[]int, v int) { + if a != data { + panic(vm.NewTypeError("a != data")) + } + *a = append(*a, v) + }) + + vm.testScriptWithTestLib(` + assert.sameValue(data.length, 1); + + // modify with js + data.push(1); + assert.sameValue(data.length, 2); + + // modify with go + append(data, 2); + assert.sameValue(data.length, 3); + `, _undefined, t) +} + +func BenchmarkGoSliceReflectSet(b *testing.B) { + vm := New() + a := vm.ToValue([]int{1}).(*Object) + b.ResetTimer() + v := intToValue(0) + for i := 0; i < b.N; i++ { + a.Set("0", v) + } +} diff --git a/goja/object_goslice_test.go b/goja/object_goslice_test.go new file mode 100644 index 0000000..1bef479 --- /dev/null +++ b/goja/object_goslice_test.go @@ -0,0 +1,298 @@ +package goja + +import ( + "testing" +) + +func TestGoSliceBasic(t *testing.T) { + const SCRIPT = ` + var sum = 0; + for (var i = 0; i < a.length; i++) { + sum += a[i]; + } + sum; + ` + r := New() + r.Set("a", []interface{}{1, 2, 3, 4}) + v, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + if i := v.ToInteger(); i != 10 { + t.Fatalf("Expected 10, got: %d", i) + } +} + +func TestGoSliceIn(t *testing.T) { + const SCRIPT = ` + var idx = ""; + for (var i in a) { + idx += i; + } + idx; + ` + r := New() + r.Set("a", []interface{}{1, 2, 3, 4}) + v, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + if i := v.String(); i != "0123" { + t.Fatalf("Expected '0123', got: '%s'", i) + } +} + +func TestGoSliceExpand(t *testing.T) { + const SCRIPT = ` + var l = a.length; + for (var i = 0; i < l; i++) { + a[l + i] = a[i] * 2; + } + + var sum = 0; + for (var i = 0; i < a.length; i++) { + sum += a[i]; + } + sum; + ` + r := New() + a := []interface{}{int64(1), int64(2), int64(3), int64(4)} + r.Set("a", &a) + v, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + sum := int64(0) + for _, v := range a { + sum += v.(int64) + } + if i := v.ToInteger(); i != sum { + t.Fatalf("Expected %d, got: %d", sum, i) + } +} + +func TestGoSliceProtoMethod(t *testing.T) { + const SCRIPT = ` + a.join(",") + ` + + r := New() + a := []interface{}{1, 2, 3, 4} + r.Set("a", a) + ret, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + if s := ret.String(); s != "1,2,3,4" { + t.Fatalf("Unexpected result: '%s'", s) + } +} + +func TestGoSliceSetLength(t *testing.T) { + r := New() + a := []interface{}{1, 2, 3, 4} + r.Set("a", &a) + _, err := r.RunString(` + 'use strict'; + a.length = 3; + if (a.length !== 3) { + throw new Error("length="+a.length); + } + if (a[3] !== undefined) { + throw new Error("a[3](1)="+a[3]); + } + a.length = 5; + if (a.length !== 5) { + throw new Error("length="+a.length); + } + if (a[3] !== null) { + throw new Error("a[3](2)="+a[3]); + } + if (a[4] !== null) { + throw new Error("a[4]="+a[4]); + } + `) + if err != nil { + t.Fatal(err) + } +} + +func TestGoSliceProto(t *testing.T) { + r := New() + a := []interface{}{1, nil, 3} + r.Set("a", &a) + r.testScriptWithTestLib(` + var proto = [,2,,4]; + Object.setPrototypeOf(a, proto); + assert.sameValue(a[1], null, "a[1]"); + assert.sameValue(a[3], 4, "a[3]"); + var desc = Object.getOwnPropertyDescriptor(a, "1"); + assert.sameValue(desc.value, null, "desc.value"); + assert(desc.writable, "writable"); + assert(desc.enumerable, "enumerable"); + assert(!desc.configurable, "configurable"); + var v5; + Object.defineProperty(proto, "5", { + set: function(v) { + v5 = v; + } + }); + a[5] = "test"; + assert.sameValue(v5, "test", "v5"); + `, _undefined, t) +} + +func TestGoSliceProtoProto(t *testing.T) { + r := New() + a := []interface{}{1, nil, 3} + proto := []interface{}{1, 2, 3, 4} + r.Set("a", &a) + r.Set("proto", proto) + _, err := r.RunString(` + "use strict"; + var protoproto = Object.create(null); + Object.defineProperty(protoproto, "3", { + value: 42 + }); + Object.setPrototypeOf(proto, protoproto); + Object.setPrototypeOf(a, proto); + a[3] = 11; + if (a[3] !== 11) { + throw new Error("a[3]=" + a[3]); + } + `) + if err != nil { + t.Fatal(err) + } +} + +func TestGoSliceDelete(t *testing.T) { + r := New() + a := []interface{}{1, nil, 3} + r.Set("a", a) + v, err := r.RunString(` + delete a[0] && delete a[1] && delete a[3]; + `) + if err != nil { + t.Fatal(err) + } + if v != valueTrue { + t.Fatalf("not true: %v", v) + } +} + +func TestGoSlicePop(t *testing.T) { + r := New() + a := []interface{}{1, nil, 3} + r.Set("a", &a) + v, err := r.RunString(` + a.pop() + `) + if err != nil { + t.Fatal(err) + } + if !v.SameAs(intToValue(3)) { + t.Fatal(v) + } +} + +func TestGoSlicePopNoPtr(t *testing.T) { + r := New() + a := []interface{}{1, nil, 3} + r.Set("a", a) + v, err := r.RunString(` + a.pop() + `) + if err != nil { + t.Fatal(err) + } + if !v.SameAs(intToValue(3)) { + t.Fatal(v) + } +} + +func TestGoSliceShift(t *testing.T) { + r := New() + a := []interface{}{1, nil, 3} + r.Set("a", &a) + v, err := r.RunString(` + a.shift() + `) + if err != nil { + t.Fatal(err) + } + if !v.SameAs(intToValue(1)) { + t.Fatal(v) + } +} + +func TestGoSliceLengthProperty(t *testing.T) { + vm := New() + vm.Set("s", []interface{}{2, 3, 4}) + _, err := vm.RunString(` + if (!s.hasOwnProperty("length")) { + throw new Error("hasOwnProperty() returned false"); + } + let desc = Object.getOwnPropertyDescriptor(s, "length"); + if (desc.value !== 3 || !desc.writable || desc.enumerable || desc.configurable) { + throw new Error("incorrect property descriptor: " + JSON.stringify(desc)); + } + `) + if err != nil { + t.Fatal(err) + } +} + +func TestGoSliceSort(t *testing.T) { + vm := New() + s := []interface{}{4, 2, 3} + vm.Set("s", &s) + _, err := vm.RunString(`s.sort()`) + if err != nil { + t.Fatal(err) + } + if len(s) != 3 { + t.Fatalf("len: %d", len(s)) + } + if s[0] != 2 || s[1] != 3 || s[2] != 4 { + t.Fatalf("val: %v", s) + } +} + +func TestGoSliceToString(t *testing.T) { + vm := New() + s := []interface{}{4, 2, 3} + vm.Set("s", &s) + res, err := vm.RunString("`${s}`") + if err != nil { + t.Fatal(err) + } + if exp := res.Export(); exp != "4,2,3" { + t.Fatal(exp) + } +} + +func TestGoSliceExternalLenUpdate(t *testing.T) { + data := &[]interface{}{1} + + vm := New() + vm.Set("data", data) + vm.Set("append", func(a *[]interface{}, v int) { + if a != data { + panic(vm.NewTypeError("a != data")) + } + *a = append(*a, v) + }) + + vm.testScriptWithTestLib(` + assert.sameValue(data.length, 1); + + // modify with js + data.push(1); + assert.sameValue(data.length, 2); + + // modify with go + append(data, 2); + assert.sameValue(data.length, 3); + `, _undefined, t) +} diff --git a/goja/object_template.go b/goja/object_template.go new file mode 100644 index 0000000..6d42f9f --- /dev/null +++ b/goja/object_template.go @@ -0,0 +1,469 @@ +package goja + +import ( + "fmt" + "github.com/dop251/goja/unistring" + "math" + "reflect" + "sort" +) + +type templatePropFactory func(*Runtime) Value + +type objectTemplate struct { + propNames []unistring.String + props map[unistring.String]templatePropFactory + + symProps map[*Symbol]templatePropFactory + symPropNames []*Symbol + + protoFactory func(*Runtime) *Object +} + +type templatedObject struct { + baseObject + tmpl *objectTemplate + + protoMaterialised bool +} + +type templatedFuncObject struct { + templatedObject + + f func(FunctionCall) Value + construct func(args []Value, newTarget *Object) *Object +} + +// This type exists because Array.prototype is supposed to be an array itself and I could not find +// a different way of implementing it without either introducing another layer of interfaces or hoisting +// the templates to baseObject both of which would have had a negative effect on the performance. +// The implementation is as simple as possible and is not optimised in any way, but I very much doubt anybody +// uses Array.prototype as an actual array. +type templatedArrayObject struct { + templatedObject +} + +func newObjectTemplate() *objectTemplate { + return &objectTemplate{ + props: make(map[unistring.String]templatePropFactory), + } +} + +func (t *objectTemplate) putStr(name unistring.String, f templatePropFactory) { + t.props[name] = f + t.propNames = append(t.propNames, name) +} + +func (t *objectTemplate) putSym(s *Symbol, f templatePropFactory) { + if t.symProps == nil { + t.symProps = make(map[*Symbol]templatePropFactory) + } + t.symProps[s] = f + t.symPropNames = append(t.symPropNames, s) +} + +func (r *Runtime) newTemplatedObject(tmpl *objectTemplate, obj *Object) *templatedObject { + if obj == nil { + obj = &Object{runtime: r} + } + o := &templatedObject{ + baseObject: baseObject{ + class: classObject, + val: obj, + extensible: true, + }, + tmpl: tmpl, + } + obj.self = o + o.init() + return o +} + +func (o *templatedObject) materialiseProto() { + if !o.protoMaterialised { + if o.tmpl.protoFactory != nil { + o.prototype = o.tmpl.protoFactory(o.val.runtime) + } + o.protoMaterialised = true + } +} + +func (o *templatedObject) getStr(name unistring.String, receiver Value) Value { + ownProp := o.getOwnPropStr(name) + if ownProp == nil { + o.materialiseProto() + } + return o.getStrWithOwnProp(ownProp, name, receiver) +} + +func (o *templatedObject) getSym(s *Symbol, receiver Value) Value { + ownProp := o.getOwnPropSym(s) + if ownProp == nil { + o.materialiseProto() + } + return o.getWithOwnProp(ownProp, s, receiver) +} + +func (o *templatedObject) getOwnPropStr(p unistring.String) Value { + if v, exists := o.values[p]; exists { + return v + } + if f := o.tmpl.props[p]; f != nil { + v := f(o.val.runtime) + o.values[p] = v + return v + } + return nil +} + +func (o *templatedObject) materialiseSymbols() { + if o.symValues == nil { + o.symValues = newOrderedMap(nil) + for _, p := range o.tmpl.symPropNames { + o.symValues.set(p, o.tmpl.symProps[p](o.val.runtime)) + } + } +} + +func (o *templatedObject) getOwnPropSym(s *Symbol) Value { + if o.symValues == nil && o.tmpl.symProps[s] == nil { + return nil + } + o.materialiseSymbols() + return o.baseObject.getOwnPropSym(s) +} + +func (o *templatedObject) materialisePropNames() { + if o.propNames == nil { + o.propNames = append(([]unistring.String)(nil), o.tmpl.propNames...) + } +} + +func (o *templatedObject) setOwnStr(p unistring.String, v Value, throw bool) bool { + existing := o.getOwnPropStr(p) // materialise property (in case it's an accessor) + if existing == nil { + o.materialiseProto() + o.materialisePropNames() + } + return o.baseObject.setOwnStr(p, v, throw) +} + +func (o *templatedObject) setOwnSym(name *Symbol, val Value, throw bool) bool { + o.materialiseSymbols() + o.materialiseProto() + return o.baseObject.setOwnSym(name, val, throw) +} + +func (o *templatedObject) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { + ownProp := o.getOwnPropStr(name) + if ownProp == nil { + o.materialiseProto() + } + return o._setForeignStr(name, ownProp, val, receiver, throw) +} + +func (o *templatedObject) proto() *Object { + o.materialiseProto() + return o.prototype +} + +func (o *templatedObject) setProto(proto *Object, throw bool) bool { + o.protoMaterialised = true + ret := o.baseObject.setProto(proto, throw) + if ret { + o.protoMaterialised = true + } + return ret +} + +func (o *templatedObject) setForeignIdx(name valueInt, val, receiver Value, throw bool) (bool, bool) { + return o.setForeignStr(name.string(), val, receiver, throw) +} + +func (o *templatedObject) setForeignSym(name *Symbol, val, receiver Value, throw bool) (bool, bool) { + o.materialiseProto() + o.materialiseSymbols() + return o.baseObject.setForeignSym(name, val, receiver, throw) +} + +func (o *templatedObject) hasPropertyStr(name unistring.String) bool { + if o.val.self.hasOwnPropertyStr(name) { + return true + } + o.materialiseProto() + if o.prototype != nil { + return o.prototype.self.hasPropertyStr(name) + } + return false +} + +func (o *templatedObject) hasPropertySym(s *Symbol) bool { + if o.hasOwnPropertySym(s) { + return true + } + o.materialiseProto() + if o.prototype != nil { + return o.prototype.self.hasPropertySym(s) + } + return false +} + +func (o *templatedObject) hasOwnPropertyStr(name unistring.String) bool { + if v, exists := o.values[name]; exists { + return v != nil + } + + _, exists := o.tmpl.props[name] + return exists +} + +func (o *templatedObject) hasOwnPropertySym(s *Symbol) bool { + if o.symValues != nil { + return o.symValues.has(s) + } + _, exists := o.tmpl.symProps[s] + return exists +} + +func (o *templatedObject) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + existingVal := o.getOwnPropStr(name) + if v, ok := o._defineOwnProperty(name, existingVal, descr, throw); ok { + o.values[name] = v + if existingVal == nil { + o.materialisePropNames() + names := copyNamesIfNeeded(o.propNames, 1) + o.propNames = append(names, name) + } + return true + } + return false +} + +func (o *templatedObject) defineOwnPropertySym(s *Symbol, descr PropertyDescriptor, throw bool) bool { + o.materialiseSymbols() + return o.baseObject.defineOwnPropertySym(s, descr, throw) +} + +func (o *templatedObject) deleteStr(name unistring.String, throw bool) bool { + if val := o.getOwnPropStr(name); val != nil { + if !o.checkDelete(name, val, throw) { + return false + } + o.materialisePropNames() + o._delete(name) + if _, exists := o.tmpl.props[name]; exists { + o.values[name] = nil // white hole + } + } + return true +} + +func (o *templatedObject) deleteSym(s *Symbol, throw bool) bool { + o.materialiseSymbols() + return o.baseObject.deleteSym(s, throw) +} + +func (o *templatedObject) materialiseProps() { + for name, f := range o.tmpl.props { + if _, exists := o.values[name]; !exists { + o.values[name] = f(o.val.runtime) + } + } + o.materialisePropNames() +} + +func (o *templatedObject) iterateStringKeys() iterNextFunc { + o.materialiseProps() + return o.baseObject.iterateStringKeys() +} + +func (o *templatedObject) iterateSymbols() iterNextFunc { + o.materialiseSymbols() + return o.baseObject.iterateSymbols() +} + +func (o *templatedObject) stringKeys(all bool, keys []Value) []Value { + if all { + o.materialisePropNames() + } else { + o.materialiseProps() + } + return o.baseObject.stringKeys(all, keys) +} + +func (o *templatedObject) symbols(all bool, accum []Value) []Value { + o.materialiseSymbols() + return o.baseObject.symbols(all, accum) +} + +func (o *templatedObject) keys(all bool, accum []Value) []Value { + return o.symbols(all, o.stringKeys(all, accum)) +} + +func (r *Runtime) newTemplatedFuncObject(tmpl *objectTemplate, obj *Object, f func(FunctionCall) Value, ctor func([]Value, *Object) *Object) *templatedFuncObject { + if obj == nil { + obj = &Object{runtime: r} + } + o := &templatedFuncObject{ + templatedObject: templatedObject{ + baseObject: baseObject{ + class: classFunction, + val: obj, + extensible: true, + }, + tmpl: tmpl, + }, + f: f, + construct: ctor, + } + obj.self = o + o.init() + return o +} + +func (f *templatedFuncObject) source() String { + return newStringValue(fmt.Sprintf("function %s() { [native code] }", nilSafe(f.getStr("name", nil)).toString())) +} + +func (f *templatedFuncObject) export(*objectExportCtx) interface{} { + return f.f +} + +func (f *templatedFuncObject) assertCallable() (func(FunctionCall) Value, bool) { + if f.f != nil { + return f.f, true + } + return nil, false +} + +func (f *templatedFuncObject) vmCall(vm *vm, n int) { + var nf nativeFuncObject + nf.f = f.f + nf.vmCall(vm, n) +} + +func (f *templatedFuncObject) assertConstructor() func(args []Value, newTarget *Object) *Object { + return f.construct +} + +func (f *templatedFuncObject) exportType() reflect.Type { + return reflectTypeFunc +} + +func (f *templatedFuncObject) typeOf() String { + return stringFunction +} + +func (f *templatedFuncObject) hasInstance(v Value) bool { + return hasInstance(f.val, v) +} + +func (r *Runtime) newTemplatedArrayObject(tmpl *objectTemplate, obj *Object) *templatedArrayObject { + if obj == nil { + obj = &Object{runtime: r} + } + o := &templatedArrayObject{ + templatedObject: templatedObject{ + baseObject: baseObject{ + class: classArray, + val: obj, + extensible: true, + }, + tmpl: tmpl, + }, + } + obj.self = o + o.init() + return o +} + +func (a *templatedArrayObject) getLenProp() *valueProperty { + lenProp, _ := a.getOwnPropStr("length").(*valueProperty) + if lenProp == nil { + panic(a.val.runtime.NewTypeError("missing length property")) + } + return lenProp +} + +func (a *templatedArrayObject) _setOwnIdx(idx uint32) { + lenProp := a.getLenProp() + l := uint32(lenProp.value.ToInteger()) + if idx >= l { + lenProp.value = intToValue(int64(idx) + 1) + } +} + +func (a *templatedArrayObject) setLength(l uint32, throw bool) bool { + lenProp := a.getLenProp() + oldLen := uint32(lenProp.value.ToInteger()) + if l == oldLen { + return true + } + if !lenProp.writable { + a.val.runtime.typeErrorResult(throw, "length is not writable") + return false + } + ret := true + if l < oldLen { + a.materialisePropNames() + a.fixPropOrder() + i := sort.Search(a.idxPropCount, func(idx int) bool { + return strToArrayIdx(a.propNames[idx]) >= l + }) + for j := a.idxPropCount - 1; j >= i; j-- { + if !a.deleteStr(a.propNames[j], false) { + l = strToArrayIdx(a.propNames[j]) + 1 + ret = false + break + } + } + } + lenProp.value = intToValue(int64(l)) + return ret +} + +func (a *templatedArrayObject) setOwnStr(name unistring.String, value Value, throw bool) bool { + if name == "length" { + return a.setLength(a.val.runtime.toLengthUint32(value), throw) + } + if !a.templatedObject.setOwnStr(name, value, throw) { + return false + } + if idx := strToArrayIdx(name); idx != math.MaxUint32 { + a._setOwnIdx(idx) + } + return true +} + +func (a *templatedArrayObject) setOwnIdx(p valueInt, v Value, throw bool) bool { + if !a.templatedObject.setOwnStr(p.string(), v, throw) { + return false + } + if idx := toIdx(p); idx != math.MaxUint32 { + a._setOwnIdx(idx) + } + return true +} + +func (a *templatedArrayObject) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + if name == "length" { + return a.val.runtime.defineArrayLength(a.getLenProp(), descr, a.setLength, throw) + } + if !a.templatedObject.defineOwnPropertyStr(name, descr, throw) { + return false + } + if idx := strToArrayIdx(name); idx != math.MaxUint32 { + a._setOwnIdx(idx) + } + return true +} + +func (a *templatedArrayObject) defineOwnPropertyIdx(p valueInt, desc PropertyDescriptor, throw bool) bool { + if !a.templatedObject.defineOwnPropertyStr(p.string(), desc, throw) { + return false + } + if idx := toIdx(p); idx != math.MaxUint32 { + a._setOwnIdx(idx) + } + return true +} diff --git a/goja/object_test.go b/goja/object_test.go new file mode 100644 index 0000000..45b95bc --- /dev/null +++ b/goja/object_test.go @@ -0,0 +1,668 @@ +package goja + +import ( + "fmt" + "reflect" + "strings" + "testing" +) + +func TestDefineProperty(t *testing.T) { + r := New() + o := r.NewObject() + + err := o.DefineDataProperty("data", r.ToValue(42), FLAG_TRUE, FLAG_TRUE, FLAG_TRUE) + if err != nil { + t.Fatal(err) + } + + err = o.DefineAccessorProperty("accessor_ro", r.ToValue(func() int { + return 1 + }), nil, FLAG_TRUE, FLAG_TRUE) + if err != nil { + t.Fatal(err) + } + + err = o.DefineAccessorProperty("accessor_rw", + r.ToValue(func(call FunctionCall) Value { + return o.Get("__hidden") + }), + r.ToValue(func(call FunctionCall) (ret Value) { + o.Set("__hidden", call.Argument(0)) + return + }), + FLAG_TRUE, FLAG_TRUE) + + if err != nil { + t.Fatal(err) + } + + if v := o.Get("accessor_ro"); v.ToInteger() != 1 { + t.Fatalf("Unexpected accessor value: %v", v) + } + + err = o.Set("accessor_ro", r.ToValue(2)) + if err == nil { + t.Fatal("Expected an error") + } + if ex, ok := err.(*Exception); ok { + if msg := ex.Error(); msg != "TypeError: Cannot assign to read only property 'accessor_ro'" { + t.Fatalf("Unexpected error: '%s'", msg) + } + } else { + t.Fatalf("Unexected error type: %T", err) + } + + err = o.Set("accessor_rw", 42) + if err != nil { + t.Fatal(err) + } + + if v := o.Get("accessor_rw"); v.ToInteger() != 42 { + t.Fatalf("Unexpected value: %v", v) + } +} + +func TestPropertyOrder(t *testing.T) { + const SCRIPT = ` + var o = {}; + var sym1 = Symbol(1); + var sym2 = Symbol(2); + o[sym2] = 1; + o[4294967294] = 1; + o[2] = 1; + o[1] = 1; + o[0] = 1; + o["02"] = 1; + o[4294967295] = 1; + o["01"] = 1; + o["00"] = 1; + o[sym1] = 1; + var expected = ["0", "1", "2", "4294967294", "02", "4294967295", "01", "00", sym2, sym1]; + var actual = Reflect.ownKeys(o); + if (actual.length !== expected.length) { + throw new Error("Unexpected length: "+actual.length); + } + for (var i = 0; i < actual.length; i++) { + if (actual[i] !== expected[i]) { + throw new Error("Unexpected list: " + actual); + } + } + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestDefinePropertiesSymbol(t *testing.T) { + const SCRIPT = ` + var desc = {}; + desc[Symbol.toStringTag] = {value: "Test"}; + var o = {}; + Object.defineProperties(o, desc); + o[Symbol.toStringTag] === "Test"; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestObjectShorthandProperties(t *testing.T) { + const SCRIPT = ` + var b = 1; + var a = {b, get() {return "c"}}; + + assert.sameValue(a.b, b, "#1"); + assert.sameValue(a.get(), "c", "#2"); + + var obj = { + w\u0069th() { return 42; } + }; + + assert.sameValue(obj['with'](), 42, 'property exists'); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestObjectAssign(t *testing.T) { + const SCRIPT = ` + assert.sameValue(Object.assign({ b: 1 }, { get a() { + Object.defineProperty(this, "b", { + value: 3, + enumerable: false + }); + }, b: 2 }).b, 1, "#1"); + + assert.sameValue(Object.assign({ b: 1 }, { get a() { + delete this.b; + }, b: 2 }).b, 1, "#2"); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestExportCircular(t *testing.T) { + vm := New() + o := vm.NewObject() + o.Set("o", o) + v := o.Export() + if m, ok := v.(map[string]interface{}); ok { + if reflect.ValueOf(m["o"]).Pointer() != reflect.ValueOf(v).Pointer() { + t.Fatal("Unexpected value") + } + } else { + t.Fatal("Unexpected type") + } + + res, err := vm.RunString(`var a = []; a[0] = a;`) + if err != nil { + t.Fatal(err) + } + v = res.Export() + if a, ok := v.([]interface{}); ok { + if reflect.ValueOf(a[0]).Pointer() != reflect.ValueOf(v).Pointer() { + t.Fatal("Unexpected value") + } + } else { + t.Fatal("Unexpected type") + } +} + +type test_s struct { + S *test_s1 +} +type test_s1 struct { + S *test_s +} + +func TestExportToCircular(t *testing.T) { + vm := New() + o := vm.NewObject() + o.Set("o", o) + var m map[string]interface{} + err := vm.ExportTo(o, &m) + if err != nil { + t.Fatal(err) + } + + type K string + type T map[K]T + var m1 T + err = vm.ExportTo(o, &m1) + if err != nil { + t.Fatal(err) + } + + type A []A + var a A + res, err := vm.RunString("var a = []; a[0] = a;") + if err != nil { + t.Fatal(err) + } + err = vm.ExportTo(res, &a) + if err != nil { + t.Fatal(err) + } + if &a[0] != &a[0][0] { + t.Fatal("values do not match") + } + + o = vm.NewObject() + o.Set("S", o) + var s test_s + err = vm.ExportTo(o, &s) + if err != nil { + t.Fatal(err) + } + if s.S.S != &s { + t.Fatalf("values do not match: %v, %v", s.S.S, &s) + } + + type test_s2 struct { + S interface{} + S1 *test_s2 + } + + var s2 test_s2 + o.Set("S1", o) + + err = vm.ExportTo(o, &s2) + if err != nil { + t.Fatal(err) + } + + if m, ok := s2.S.(map[string]interface{}); ok { + if reflect.ValueOf(m["S"]).Pointer() != reflect.ValueOf(m).Pointer() { + t.Fatal("Unexpected m.S") + } + } else { + t.Fatalf("Unexpected s2.S type: %T", s2.S) + } + if s2.S1 != &s2 { + t.Fatal("Unexpected s2.S1") + } + + o1 := vm.NewObject() + o1.Set("S", o) + o1.Set("S1", o) + err = vm.ExportTo(o1, &s2) + if err != nil { + t.Fatal(err) + } + if s2.S1.S1 != s2.S1 { + t.Fatal("Unexpected s2.S1.S1") + } +} + +func TestExportWrappedMap(t *testing.T) { + vm := New() + m := map[string]interface{}{ + "test": "failed", + } + exported := vm.ToValue(m).Export() + if exportedMap, ok := exported.(map[string]interface{}); ok { + exportedMap["test"] = "passed" + if v := m["test"]; v != "passed" { + t.Fatalf("Unexpected m[\"test\"]: %v", v) + } + } else { + t.Fatalf("Unexpected export type: %T", exported) + } +} + +func TestExportToWrappedMap(t *testing.T) { + vm := New() + m := map[string]interface{}{ + "test": "failed", + } + var exported map[string]interface{} + err := vm.ExportTo(vm.ToValue(m), &exported) + if err != nil { + t.Fatal(err) + } + exported["test"] = "passed" + if v := m["test"]; v != "passed" { + t.Fatalf("Unexpected m[\"test\"]: %v", v) + } +} + +func TestExportToWrappedMapCustom(t *testing.T) { + type CustomMap map[string]bool + vm := New() + m := CustomMap{} + var exported CustomMap + err := vm.ExportTo(vm.ToValue(m), &exported) + if err != nil { + t.Fatal(err) + } + exported["test"] = true + if v := m["test"]; v != true { + t.Fatalf("Unexpected m[\"test\"]: %v", v) + } +} + +func TestExportToSliceNonIterable(t *testing.T) { + vm := New() + o := vm.NewObject() + var a []interface{} + err := vm.ExportTo(o, &a) + if err == nil { + t.Fatal("Expected an error") + } + if len(a) != 0 { + t.Fatalf("a: %v", a) + } + if msg := err.Error(); msg != "cannot convert [object Object] to []interface {}: not an array or iterable" { + t.Fatalf("Unexpected error: %v", err) + } +} + +func ExampleRuntime_ExportTo_iterableToSlice() { + vm := New() + v, err := vm.RunString(` + function reverseIterator() { + const arr = this; + let idx = arr.length; + return { + next: () => idx > 0 ? {value: arr[--idx]} : {done: true} + } + } + const arr = [1,2,3]; + arr[Symbol.iterator] = reverseIterator; + arr; + `) + if err != nil { + panic(err) + } + + var arr []int + err = vm.ExportTo(v, &arr) + if err != nil { + panic(err) + } + + fmt.Println(arr) + // Output: [3 2 1] +} + +func TestRuntime_ExportTo_proxiedIterableToSlice(t *testing.T) { + vm := New() + v, err := vm.RunString(` + function reverseIterator() { + const arr = this; + let idx = arr.length; + return { + next: () => idx > 0 ? {value: arr[--idx]} : {done: true} + } + } + const arr = [1,2,3]; + arr[Symbol.iterator] = reverseIterator; + new Proxy(arr, {}); + `) + if err != nil { + t.Fatal(err) + } + + var arr []int + err = vm.ExportTo(v, &arr) + if err != nil { + t.Fatal(err) + } + if out := fmt.Sprint(arr); out != "[3 2 1]" { + t.Fatal(out) + } +} + +func ExampleRuntime_ExportTo_arrayLikeToSlice() { + vm := New() + v, err := vm.RunString(` + ({ + length: 3, + 0: 1, + 1: 2, + 2: 3 + }); + `) + if err != nil { + panic(err) + } + + var arr []int + err = vm.ExportTo(v, &arr) + if err != nil { + panic(err) + } + + fmt.Println(arr) + // Output: [1 2 3] +} + +func TestExportArrayToArrayMismatchedLengths(t *testing.T) { + vm := New() + a := vm.NewArray(1, 2) + var a1 [3]int + err := vm.ExportTo(a, &a1) + if err == nil { + t.Fatal("expected error") + } + if msg := err.Error(); !strings.Contains(msg, "lengths mismatch") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestExportIterableToArrayMismatchedLengths(t *testing.T) { + vm := New() + a, err := vm.RunString(` + new Map([[1, true], [2, true]]); + `) + if err != nil { + t.Fatal(err) + } + + var a1 [3]interface{} + err = vm.ExportTo(a, &a1) + if err == nil { + t.Fatal("expected error") + } + if msg := err.Error(); !strings.Contains(msg, "lengths mismatch") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestExportArrayLikeToArrayMismatchedLengths(t *testing.T) { + vm := New() + a, err := vm.RunString(` + ({ + length: 2, + 0: true, + 1: true + }); + `) + if err != nil { + t.Fatal(err) + } + + var a1 [3]interface{} + err = vm.ExportTo(a, &a1) + if err == nil { + t.Fatal("expected error") + } + if msg := err.Error(); !strings.Contains(msg, "lengths mismatch") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestSetForeignReturnValue(t *testing.T) { + const SCRIPT = ` + var array = [1, 2, 3]; + var arrayTarget = new Proxy(array, {}); + + Object.preventExtensions(array); + + !Reflect.set(arrayTarget, "foo", 2); + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestDefinePropertiesUndefinedVal(t *testing.T) { + const SCRIPT = ` +var target = {}; +var sym = Symbol(); +target[sym] = 1; +target.foo = 2; +target[0] = 3; + +var getOwnKeys = []; +var proxy = new Proxy(target, { + getOwnPropertyDescriptor: function(_target, key) { + getOwnKeys.push(key); + }, +}); + +Object.defineProperties({}, proxy); + true; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func ExampleObject_Delete() { + vm := New() + obj := vm.NewObject() + _ = obj.Set("test", true) + before := obj.Get("test") + _ = obj.Delete("test") + after := obj.Get("test") + fmt.Printf("before: %v, after: %v", before, after) + // Output: before: true, after: +} + +func ExampleObject_GetOwnPropertyNames() { + vm := New() + obj := vm.NewObject() + obj.DefineDataProperty("test", vm.ToValue(true), FLAG_TRUE, FLAG_TRUE, FLAG_FALSE) // define a non-enumerable property that Keys() would not return + fmt.Print(obj.GetOwnPropertyNames()) + // Output: [test] +} + +func TestObjectEquality(t *testing.T) { + type CustomInt int + type S struct { + F CustomInt + } + vm := New() + vm.Set("s", S{}) + // indexOf() and includes() use different equality checks (StrictEquals and SameValueZero respectively), + // but for objects they must behave the same. De-referencing s.F creates a new wrapper each time. + vm.testScriptWithTestLib(` + assert.sameValue([s.F].indexOf(s.F), 0, "indexOf"); + assert([s.F].includes(s.F)); + `, _undefined, t) +} + +func BenchmarkPut(b *testing.B) { + v := &Object{} + + o := &baseObject{ + val: v, + extensible: true, + } + v.self = o + + o.init() + + var key Value = asciiString("test") + var val Value = valueInt(123) + + for i := 0; i < b.N; i++ { + v.setOwn(key, val, false) + } +} + +func BenchmarkPutStr(b *testing.B) { + v := &Object{} + + o := &baseObject{ + val: v, + extensible: true, + } + + o.init() + + v.self = o + + var val Value = valueInt(123) + + for i := 0; i < b.N; i++ { + o.setOwnStr("test", val, false) + } +} + +func BenchmarkGet(b *testing.B) { + v := &Object{} + + o := &baseObject{ + val: v, + extensible: true, + } + + o.init() + + v.self = o + var n Value = asciiString("test") + + for i := 0; i < b.N; i++ { + v.get(n, nil) + } + +} + +func BenchmarkGetStr(b *testing.B) { + v := &Object{} + + o := &baseObject{ + val: v, + extensible: true, + } + v.self = o + + o.init() + + for i := 0; i < b.N; i++ { + o.getStr("test", nil) + } +} + +func __toString(v Value) string { + switch v := v.(type) { + case asciiString: + return string(v) + default: + return "" + } +} + +func BenchmarkToString1(b *testing.B) { + v := asciiString("test") + + for i := 0; i < b.N; i++ { + v.toString() + } +} + +func BenchmarkToString2(b *testing.B) { + v := asciiString("test") + + for i := 0; i < b.N; i++ { + __toString(v) + } +} + +func BenchmarkConv(b *testing.B) { + count := int64(0) + for i := 0; i < b.N; i++ { + count += valueInt(123).ToInteger() + } + if count == 0 { + b.Fatal("zero") + } +} + +func BenchmarkToUTF8String(b *testing.B) { + var s String = asciiString("test") + for i := 0; i < b.N; i++ { + _ = s.String() + } +} + +func BenchmarkAdd(b *testing.B) { + var x, y Value + x = valueInt(2) + y = valueInt(2) + + for i := 0; i < b.N; i++ { + if xi, ok := x.(valueInt); ok { + if yi, ok := y.(valueInt); ok { + x = xi + yi + } + } + } +} + +func BenchmarkAddString(b *testing.B) { + var x, y Value + + tst := asciiString("22") + x = asciiString("2") + y = asciiString("2") + + for i := 0; i < b.N; i++ { + var z Value + if xi, ok := x.(String); ok { + if yi, ok := y.(String); ok { + z = xi.Concat(yi) + } + } + if !z.StrictEquals(tst) { + b.Fatalf("Unexpected result %v", x) + } + } +} diff --git a/goja/parser/README.markdown b/goja/parser/README.markdown new file mode 100644 index 0000000..ec1186d --- /dev/null +++ b/goja/parser/README.markdown @@ -0,0 +1,184 @@ +# parser +-- + import "github.com/dop251/goja/parser" + +Package parser implements a parser for JavaScript. Borrowed from https://github.com/robertkrimen/otto/tree/master/parser + + import ( + "github.com/dop251/goja/parser" + ) + +Parse and return an AST + + filename := "" // A filename is optional + src := ` + // Sample xyzzy example + (function(){ + if (3.14159 > 0) { + console.log("Hello, World."); + return; + } + + var xyzzy = NaN; + console.log("Nothing happens."); + return xyzzy; + })(); + ` + + // Parse some JavaScript, yielding a *ast.Program and/or an ErrorList + program, err := parser.ParseFile(nil, filename, src, 0) + + +### Warning + +The parser and AST interfaces are still works-in-progress (particularly where +node types are concerned) and may change in the future. + +## Usage + +#### func ParseFile + +```go +func ParseFile(fileSet *file.FileSet, filename string, src interface{}, mode Mode) (*ast.Program, error) +``` +ParseFile parses the source code of a single JavaScript/ECMAScript source file +and returns the corresponding ast.Program node. + +If fileSet == nil, ParseFile parses source without a FileSet. If fileSet != nil, +ParseFile first adds filename and src to fileSet. + +The filename argument is optional and is used for labelling errors, etc. + +src may be a string, a byte slice, a bytes.Buffer, or an io.Reader, but it MUST +always be in UTF-8. + + // Parse some JavaScript, yielding a *ast.Program and/or an ErrorList + program, err := parser.ParseFile(nil, "", `if (abc > 1) {}`, 0) + +#### func ParseFunction + +```go +func ParseFunction(parameterList, body string) (*ast.FunctionLiteral, error) +``` +ParseFunction parses a given parameter list and body as a function and returns +the corresponding ast.FunctionLiteral node. + +The parameter list, if any, should be a comma-separated list of identifiers. + +#### func ReadSource + +```go +func ReadSource(filename string, src interface{}) ([]byte, error) +``` + +#### func TransformRegExp + +```go +func TransformRegExp(pattern string) (string, error) +``` +TransformRegExp transforms a JavaScript pattern into a Go "regexp" pattern. + +re2 (Go) cannot do backtracking, so the presence of a lookahead (?=) (?!) or +backreference (\1, \2, ...) will cause an error. + +re2 (Go) has a different definition for \s: [\t\n\f\r ]. The JavaScript +definition, on the other hand, also includes \v, Unicode "Separator, Space", +etc. + +If the pattern is invalid (not valid even in JavaScript), then this function +returns the empty string and an error. + +If the pattern is valid, but incompatible (contains a lookahead or +backreference), then this function returns the transformation (a non-empty +string) AND an error. + +#### type Error + +```go +type Error struct { + Position file.Position + Message string +} +``` + +An Error represents a parsing error. It includes the position where the error +occurred and a message/description. + +#### func (Error) Error + +```go +func (self Error) Error() string +``` + +#### type ErrorList + +```go +type ErrorList []*Error +``` + +ErrorList is a list of *Errors. + +#### func (*ErrorList) Add + +```go +func (self *ErrorList) Add(position file.Position, msg string) +``` +Add adds an Error with given position and message to an ErrorList. + +#### func (ErrorList) Err + +```go +func (self ErrorList) Err() error +``` +Err returns an error equivalent to this ErrorList. If the list is empty, Err +returns nil. + +#### func (ErrorList) Error + +```go +func (self ErrorList) Error() string +``` +Error implements the Error interface. + +#### func (ErrorList) Len + +```go +func (self ErrorList) Len() int +``` + +#### func (ErrorList) Less + +```go +func (self ErrorList) Less(i, j int) bool +``` + +#### func (*ErrorList) Reset + +```go +func (self *ErrorList) Reset() +``` +Reset resets an ErrorList to no errors. + +#### func (ErrorList) Sort + +```go +func (self ErrorList) Sort() +``` + +#### func (ErrorList) Swap + +```go +func (self ErrorList) Swap(i, j int) +``` + +#### type Mode + +```go +type Mode uint +``` + +A Mode value is a set of flags (or 0). They control optional parser +functionality. + +-- +**godocdown** http://github.com/robertkrimen/godocdown diff --git a/goja/parser/error.go b/goja/parser/error.go new file mode 100644 index 0000000..cf4d2c3 --- /dev/null +++ b/goja/parser/error.go @@ -0,0 +1,176 @@ +package parser + +import ( + "fmt" + "sort" + + "github.com/dop251/goja/file" + "github.com/dop251/goja/token" +) + +const ( + err_UnexpectedToken = "Unexpected token %v" + err_UnexpectedEndOfInput = "Unexpected end of input" + err_UnexpectedEscape = "Unexpected escape" +) + +// UnexpectedNumber: 'Unexpected number', +// UnexpectedString: 'Unexpected string', +// UnexpectedIdentifier: 'Unexpected identifier', +// UnexpectedReserved: 'Unexpected reserved word', +// NewlineAfterThrow: 'Illegal newline after throw', +// InvalidRegExp: 'Invalid regular expression', +// UnterminatedRegExp: 'Invalid regular expression: missing /', +// InvalidLHSInAssignment: 'Invalid left-hand side in assignment', +// InvalidLHSInForIn: 'Invalid left-hand side in for-in', +// MultipleDefaultsInSwitch: 'More than one default clause in switch statement', +// NoCatchOrFinally: 'Missing catch or finally after try', +// UnknownLabel: 'Undefined label \'%0\'', +// Redeclaration: '%0 \'%1\' has already been declared', +// IllegalContinue: 'Illegal continue statement', +// IllegalBreak: 'Illegal break statement', +// IllegalReturn: 'Illegal return statement', +// StrictModeWith: 'Strict mode code may not include a with statement', +// StrictCatchVariable: 'Catch variable may not be eval or arguments in strict mode', +// StrictVarName: 'Variable name may not be eval or arguments in strict mode', +// StrictParamName: 'Parameter name eval or arguments is not allowed in strict mode', +// StrictParamDupe: 'Strict mode function may not have duplicate parameter names', +// StrictFunctionName: 'Function name may not be eval or arguments in strict mode', +// StrictOctalLiteral: 'Octal literals are not allowed in strict mode.', +// StrictDelete: 'Delete of an unqualified identifier in strict mode.', +// StrictDuplicateProperty: 'Duplicate data property in object literal not allowed in strict mode', +// AccessorDataProperty: 'Object literal may not have data and accessor property with the same name', +// AccessorGetSet: 'Object literal may not have multiple get/set accessors with the same name', +// StrictLHSAssignment: 'Assignment to eval or arguments is not allowed in strict mode', +// StrictLHSPostfix: 'Postfix increment/decrement may not have eval or arguments operand in strict mode', +// StrictLHSPrefix: 'Prefix increment/decrement may not have eval or arguments operand in strict mode', +// StrictReservedWord: 'Use of future reserved word in strict mode' + +// A SyntaxError is a description of an ECMAScript syntax error. + +// An Error represents a parsing error. It includes the position where the error occurred and a message/description. +type Error struct { + Position file.Position + Message string +} + +// FIXME Should this be "SyntaxError"? + +func (self Error) Error() string { + filename := self.Position.Filename + if filename == "" { + filename = "(anonymous)" + } + return fmt.Sprintf("%s: Line %d:%d %s", + filename, + self.Position.Line, + self.Position.Column, + self.Message, + ) +} + +func (self *_parser) error(place interface{}, msg string, msgValues ...interface{}) *Error { + idx := file.Idx(0) + switch place := place.(type) { + case int: + idx = self.idxOf(place) + case file.Idx: + if place == 0 { + idx = self.idxOf(self.chrOffset) + } else { + idx = place + } + default: + panic(fmt.Errorf("error(%T, ...)", place)) + } + + position := self.position(idx) + msg = fmt.Sprintf(msg, msgValues...) + self.errors.Add(position, msg) + return self.errors[len(self.errors)-1] +} + +func (self *_parser) errorUnexpected(idx file.Idx, chr rune) error { + if chr == -1 { + return self.error(idx, err_UnexpectedEndOfInput) + } + return self.error(idx, err_UnexpectedToken, token.ILLEGAL) +} + +func (self *_parser) errorUnexpectedToken(tkn token.Token) error { + switch tkn { + case token.EOF: + return self.error(file.Idx(0), err_UnexpectedEndOfInput) + } + value := tkn.String() + switch tkn { + case token.BOOLEAN, token.NULL: + value = self.literal + case token.IDENTIFIER: + return self.error(self.idx, "Unexpected identifier") + case token.KEYWORD: + // TODO Might be a future reserved word + return self.error(self.idx, "Unexpected reserved word") + case token.ESCAPED_RESERVED_WORD: + return self.error(self.idx, "Keyword must not contain escaped characters") + case token.NUMBER: + return self.error(self.idx, "Unexpected number") + case token.STRING: + return self.error(self.idx, "Unexpected string") + } + return self.error(self.idx, err_UnexpectedToken, value) +} + +// ErrorList is a list of *Errors. +type ErrorList []*Error + +// Add adds an Error with given position and message to an ErrorList. +func (self *ErrorList) Add(position file.Position, msg string) { + *self = append(*self, &Error{position, msg}) +} + +// Reset resets an ErrorList to no errors. +func (self *ErrorList) Reset() { *self = (*self)[0:0] } + +func (self ErrorList) Len() int { return len(self) } +func (self ErrorList) Swap(i, j int) { self[i], self[j] = self[j], self[i] } +func (self ErrorList) Less(i, j int) bool { + x := &self[i].Position + y := &self[j].Position + if x.Filename < y.Filename { + return true + } + if x.Filename == y.Filename { + if x.Line < y.Line { + return true + } + if x.Line == y.Line { + return x.Column < y.Column + } + } + return false +} + +func (self ErrorList) Sort() { + sort.Sort(self) +} + +// Error implements the Error interface. +func (self ErrorList) Error() string { + switch len(self) { + case 0: + return "no errors" + case 1: + return self[0].Error() + } + return fmt.Sprintf("%s (and %d more errors)", self[0].Error(), len(self)-1) +} + +// Err returns an error equivalent to this ErrorList. +// If the list is empty, Err returns nil. +func (self ErrorList) Err() error { + if len(self) == 0 { + return nil + } + return self +} diff --git a/goja/parser/expression.go b/goja/parser/expression.go new file mode 100644 index 0000000..305bed4 --- /dev/null +++ b/goja/parser/expression.go @@ -0,0 +1,1660 @@ +package parser + +import ( + "strings" + + "github.com/dop251/goja/ast" + "github.com/dop251/goja/file" + "github.com/dop251/goja/token" + "github.com/dop251/goja/unistring" +) + +func (self *_parser) parseIdentifier() *ast.Identifier { + literal := self.parsedLiteral + idx := self.idx + self.next() + return &ast.Identifier{ + Name: literal, + Idx: idx, + } +} + +func (self *_parser) parsePrimaryExpression() ast.Expression { + literal, parsedLiteral := self.literal, self.parsedLiteral + idx := self.idx + switch self.token { + case token.IDENTIFIER: + self.next() + return &ast.Identifier{ + Name: parsedLiteral, + Idx: idx, + } + case token.NULL: + self.next() + return &ast.NullLiteral{ + Idx: idx, + Literal: literal, + } + case token.BOOLEAN: + self.next() + value := false + switch parsedLiteral { + case "true": + value = true + case "false": + value = false + default: + self.error(idx, "Illegal boolean literal") + } + return &ast.BooleanLiteral{ + Idx: idx, + Literal: literal, + Value: value, + } + case token.STRING: + self.next() + return &ast.StringLiteral{ + Idx: idx, + Literal: literal, + Value: parsedLiteral, + } + case token.NUMBER: + self.next() + value, err := parseNumberLiteral(literal) + if err != nil { + self.error(idx, err.Error()) + value = 0 + } + return &ast.NumberLiteral{ + Idx: idx, + Literal: literal, + Value: value, + } + case token.SLASH, token.QUOTIENT_ASSIGN: + return self.parseRegExpLiteral() + case token.LEFT_BRACE: + return self.parseObjectLiteral() + case token.LEFT_BRACKET: + return self.parseArrayLiteral() + case token.LEFT_PARENTHESIS: + return self.parseParenthesisedExpression() + case token.BACKTICK: + return self.parseTemplateLiteral(false) + case token.THIS: + self.next() + return &ast.ThisExpression{ + Idx: idx, + } + case token.SUPER: + return self.parseSuperProperty() + case token.ASYNC: + if f := self.parseMaybeAsyncFunction(false); f != nil { + return f + } + case token.FUNCTION: + return self.parseFunction(false, false, idx) + case token.CLASS: + return self.parseClass(false) + } + + if self.isBindingId(self.token) { + self.next() + return &ast.Identifier{ + Name: parsedLiteral, + Idx: idx, + } + } + + self.errorUnexpectedToken(self.token) + self.nextStatement() + return &ast.BadExpression{From: idx, To: self.idx} +} + +func (self *_parser) parseSuperProperty() ast.Expression { + idx := self.idx + self.next() + switch self.token { + case token.PERIOD: + self.next() + if !token.IsId(self.token) { + self.expect(token.IDENTIFIER) + self.nextStatement() + return &ast.BadExpression{From: idx, To: self.idx} + } + idIdx := self.idx + parsedLiteral := self.parsedLiteral + self.next() + return &ast.DotExpression{ + Left: &ast.SuperExpression{ + Idx: idx, + }, + Identifier: ast.Identifier{ + Name: parsedLiteral, + Idx: idIdx, + }, + } + case token.LEFT_BRACKET: + return self.parseBracketMember(&ast.SuperExpression{ + Idx: idx, + }) + case token.LEFT_PARENTHESIS: + return self.parseCallExpression(&ast.SuperExpression{ + Idx: idx, + }) + default: + self.error(idx, "'super' keyword unexpected here") + self.nextStatement() + return &ast.BadExpression{From: idx, To: self.idx} + } +} + +func (self *_parser) reinterpretSequenceAsArrowFuncParams(list []ast.Expression) *ast.ParameterList { + firstRestIdx := -1 + params := make([]*ast.Binding, 0, len(list)) + for i, item := range list { + if _, ok := item.(*ast.SpreadElement); ok { + if firstRestIdx == -1 { + firstRestIdx = i + continue + } + } + if firstRestIdx != -1 { + self.error(list[firstRestIdx].Idx0(), "Rest parameter must be last formal parameter") + return &ast.ParameterList{} + } + params = append(params, self.reinterpretAsBinding(item)) + } + var rest ast.Expression + if firstRestIdx != -1 { + rest = self.reinterpretAsBindingRestElement(list[firstRestIdx]) + } + return &ast.ParameterList{ + List: params, + Rest: rest, + } +} + +func (self *_parser) parseParenthesisedExpression() ast.Expression { + opening := self.idx + self.expect(token.LEFT_PARENTHESIS) + var list []ast.Expression + if self.token != token.RIGHT_PARENTHESIS { + for { + if self.token == token.ELLIPSIS { + start := self.idx + self.errorUnexpectedToken(token.ELLIPSIS) + self.next() + expr := self.parseAssignmentExpression() + list = append(list, &ast.BadExpression{ + From: start, + To: expr.Idx1(), + }) + } else { + list = append(list, self.parseAssignmentExpression()) + } + if self.token != token.COMMA { + break + } + self.next() + if self.token == token.RIGHT_PARENTHESIS { + self.errorUnexpectedToken(token.RIGHT_PARENTHESIS) + break + } + } + } + self.expect(token.RIGHT_PARENTHESIS) + if len(list) == 1 && len(self.errors) == 0 { + return list[0] + } + if len(list) == 0 { + self.errorUnexpectedToken(token.RIGHT_PARENTHESIS) + return &ast.BadExpression{ + From: opening, + To: self.idx, + } + } + return &ast.SequenceExpression{ + Sequence: list, + } +} + +func (self *_parser) parseRegExpLiteral() *ast.RegExpLiteral { + + offset := self.chrOffset - 1 // Opening slash already gotten + if self.token == token.QUOTIENT_ASSIGN { + offset -= 1 // = + } + idx := self.idxOf(offset) + + pattern, _, err := self.scanString(offset, false) + endOffset := self.chrOffset + + if err == "" { + pattern = pattern[1 : len(pattern)-1] + } + + flags := "" + if !isLineTerminator(self.chr) && !isLineWhiteSpace(self.chr) { + self.next() + + if self.token == token.IDENTIFIER { // gim + + flags = self.literal + self.next() + endOffset = self.chrOffset - 1 + } + } else { + self.next() + } + + literal := self.str[offset:endOffset] + + return &ast.RegExpLiteral{ + Idx: idx, + Literal: literal, + Pattern: pattern, + Flags: flags, + } +} + +func (self *_parser) isBindingId(tok token.Token) bool { + if tok == token.IDENTIFIER { + return true + } + + if tok == token.AWAIT { + return !self.scope.allowAwait + } + if tok == token.YIELD { + return !self.scope.allowYield + } + + if token.IsUnreservedWord(tok) { + return true + } + return false +} + +func (self *_parser) tokenToBindingId() { + if self.isBindingId(self.token) { + self.token = token.IDENTIFIER + } +} + +func (self *_parser) parseBindingTarget() (target ast.BindingTarget) { + self.tokenToBindingId() + switch self.token { + case token.IDENTIFIER: + target = &ast.Identifier{ + Name: self.parsedLiteral, + Idx: self.idx, + } + self.next() + case token.LEFT_BRACKET: + target = self.parseArrayBindingPattern() + case token.LEFT_BRACE: + target = self.parseObjectBindingPattern() + default: + idx := self.expect(token.IDENTIFIER) + self.nextStatement() + target = &ast.BadExpression{From: idx, To: self.idx} + } + + return +} + +func (self *_parser) parseVariableDeclaration(declarationList *[]*ast.Binding) *ast.Binding { + node := &ast.Binding{ + Target: self.parseBindingTarget(), + } + + if declarationList != nil { + *declarationList = append(*declarationList, node) + } + + if self.token == token.ASSIGN { + self.next() + node.Initializer = self.parseAssignmentExpression() + } + + return node +} + +func (self *_parser) parseVariableDeclarationList() (declarationList []*ast.Binding) { + for { + self.parseVariableDeclaration(&declarationList) + if self.token != token.COMMA { + break + } + self.next() + } + return +} + +func (self *_parser) parseVarDeclarationList(var_ file.Idx) []*ast.Binding { + declarationList := self.parseVariableDeclarationList() + + self.scope.declare(&ast.VariableDeclaration{ + Var: var_, + List: declarationList, + }) + + return declarationList +} + +func (self *_parser) parseObjectPropertyKey() (string, unistring.String, ast.Expression, token.Token) { + if self.token == token.LEFT_BRACKET { + self.next() + expr := self.parseAssignmentExpression() + self.expect(token.RIGHT_BRACKET) + return "", "", expr, token.ILLEGAL + } + idx, tkn, literal, parsedLiteral := self.idx, self.token, self.literal, self.parsedLiteral + var value ast.Expression + self.next() + switch tkn { + case token.IDENTIFIER, token.STRING, token.KEYWORD, token.ESCAPED_RESERVED_WORD: + value = &ast.StringLiteral{ + Idx: idx, + Literal: literal, + Value: parsedLiteral, + } + case token.NUMBER: + num, err := parseNumberLiteral(literal) + if err != nil { + self.error(idx, err.Error()) + } else { + value = &ast.NumberLiteral{ + Idx: idx, + Literal: literal, + Value: num, + } + } + case token.PRIVATE_IDENTIFIER: + value = &ast.PrivateIdentifier{ + Identifier: ast.Identifier{ + Idx: idx, + Name: parsedLiteral, + }, + } + default: + // null, false, class, etc. + if token.IsId(tkn) { + value = &ast.StringLiteral{ + Idx: idx, + Literal: literal, + Value: unistring.String(literal), + } + } else { + self.errorUnexpectedToken(tkn) + } + } + return literal, parsedLiteral, value, tkn +} + +func (self *_parser) parseObjectProperty() ast.Property { + if self.token == token.ELLIPSIS { + self.next() + return &ast.SpreadElement{ + Expression: self.parseAssignmentExpression(), + } + } + keyStartIdx := self.idx + generator := false + if self.token == token.MULTIPLY { + generator = true + self.next() + } + literal, parsedLiteral, value, tkn := self.parseObjectPropertyKey() + if value == nil { + return nil + } + if token.IsId(tkn) || tkn == token.STRING || tkn == token.NUMBER || tkn == token.ILLEGAL { + if generator { + return &ast.PropertyKeyed{ + Key: value, + Kind: ast.PropertyKindMethod, + Value: self.parseMethodDefinition(keyStartIdx, ast.PropertyKindMethod, true, false), + Computed: tkn == token.ILLEGAL, + } + } + switch { + case self.token == token.LEFT_PARENTHESIS: + return &ast.PropertyKeyed{ + Key: value, + Kind: ast.PropertyKindMethod, + Value: self.parseMethodDefinition(keyStartIdx, ast.PropertyKindMethod, false, false), + Computed: tkn == token.ILLEGAL, + } + case self.token == token.COMMA || self.token == token.RIGHT_BRACE || self.token == token.ASSIGN: // shorthand property + if self.isBindingId(tkn) { + var initializer ast.Expression + if self.token == token.ASSIGN { + // allow the initializer syntax here in case the object literal + // needs to be reinterpreted as an assignment pattern, enforce later if it doesn't. + self.next() + initializer = self.parseAssignmentExpression() + } + return &ast.PropertyShort{ + Name: ast.Identifier{ + Name: parsedLiteral, + Idx: value.Idx0(), + }, + Initializer: initializer, + } + } else { + self.errorUnexpectedToken(self.token) + } + case (literal == "get" || literal == "set" || tkn == token.ASYNC) && self.token != token.COLON: + _, _, keyValue, tkn1 := self.parseObjectPropertyKey() + if keyValue == nil { + return nil + } + + var kind ast.PropertyKind + var async bool + if tkn == token.ASYNC { + async = true + kind = ast.PropertyKindMethod + } else if literal == "get" { + kind = ast.PropertyKindGet + } else { + kind = ast.PropertyKindSet + } + + return &ast.PropertyKeyed{ + Key: keyValue, + Kind: kind, + Value: self.parseMethodDefinition(keyStartIdx, kind, false, async), + Computed: tkn1 == token.ILLEGAL, + } + } + } + + self.expect(token.COLON) + return &ast.PropertyKeyed{ + Key: value, + Kind: ast.PropertyKindValue, + Value: self.parseAssignmentExpression(), + Computed: tkn == token.ILLEGAL, + } +} + +func (self *_parser) parseMethodDefinition(keyStartIdx file.Idx, kind ast.PropertyKind, generator, async bool) *ast.FunctionLiteral { + idx1 := self.idx + if generator != self.scope.allowYield { + self.scope.allowYield = generator + defer func() { + self.scope.allowYield = !generator + }() + } + if async != self.scope.allowAwait { + self.scope.allowAwait = async + defer func() { + self.scope.allowAwait = !async + }() + } + parameterList := self.parseFunctionParameterList() + switch kind { + case ast.PropertyKindGet: + if len(parameterList.List) > 0 || parameterList.Rest != nil { + self.error(idx1, "Getter must not have any formal parameters.") + } + case ast.PropertyKindSet: + if len(parameterList.List) != 1 || parameterList.Rest != nil { + self.error(idx1, "Setter must have exactly one formal parameter.") + } + } + node := &ast.FunctionLiteral{ + Function: keyStartIdx, + ParameterList: parameterList, + Generator: generator, + Async: async, + } + node.Body, node.DeclarationList = self.parseFunctionBlock(async, async, generator) + node.Source = self.slice(keyStartIdx, node.Body.Idx1()) + return node +} + +func (self *_parser) parseObjectLiteral() *ast.ObjectLiteral { + var value []ast.Property + idx0 := self.expect(token.LEFT_BRACE) + for self.token != token.RIGHT_BRACE && self.token != token.EOF { + property := self.parseObjectProperty() + if property != nil { + value = append(value, property) + } + if self.token != token.RIGHT_BRACE { + self.expect(token.COMMA) + } else { + break + } + } + idx1 := self.expect(token.RIGHT_BRACE) + + return &ast.ObjectLiteral{ + LeftBrace: idx0, + RightBrace: idx1, + Value: value, + } +} + +func (self *_parser) parseArrayLiteral() *ast.ArrayLiteral { + + idx0 := self.expect(token.LEFT_BRACKET) + var value []ast.Expression + for self.token != token.RIGHT_BRACKET && self.token != token.EOF { + if self.token == token.COMMA { + self.next() + value = append(value, nil) + continue + } + if self.token == token.ELLIPSIS { + self.next() + value = append(value, &ast.SpreadElement{ + Expression: self.parseAssignmentExpression(), + }) + } else { + value = append(value, self.parseAssignmentExpression()) + } + if self.token != token.RIGHT_BRACKET { + self.expect(token.COMMA) + } + } + idx1 := self.expect(token.RIGHT_BRACKET) + + return &ast.ArrayLiteral{ + LeftBracket: idx0, + RightBracket: idx1, + Value: value, + } +} + +func (self *_parser) parseTemplateLiteral(tagged bool) *ast.TemplateLiteral { + res := &ast.TemplateLiteral{ + OpenQuote: self.idx, + } + for { + start := self.offset + literal, parsed, finished, parseErr, err := self.parseTemplateCharacters() + if err != "" { + self.error(self.offset, err) + } + res.Elements = append(res.Elements, &ast.TemplateElement{ + Idx: self.idxOf(start), + Literal: literal, + Parsed: parsed, + Valid: parseErr == "", + }) + if !tagged && parseErr != "" { + self.error(self.offset, parseErr) + } + end := self.chrOffset - 1 + self.next() + if finished { + res.CloseQuote = self.idxOf(end) + break + } + expr := self.parseExpression() + res.Expressions = append(res.Expressions, expr) + if self.token != token.RIGHT_BRACE { + self.errorUnexpectedToken(self.token) + } + } + return res +} + +func (self *_parser) parseTaggedTemplateLiteral(tag ast.Expression) *ast.TemplateLiteral { + l := self.parseTemplateLiteral(true) + l.Tag = tag + return l +} + +func (self *_parser) parseArgumentList() (argumentList []ast.Expression, idx0, idx1 file.Idx) { + idx0 = self.expect(token.LEFT_PARENTHESIS) + for self.token != token.RIGHT_PARENTHESIS { + var item ast.Expression + if self.token == token.ELLIPSIS { + self.next() + item = &ast.SpreadElement{ + Expression: self.parseAssignmentExpression(), + } + } else { + item = self.parseAssignmentExpression() + } + argumentList = append(argumentList, item) + if self.token != token.COMMA { + break + } + self.next() + } + idx1 = self.expect(token.RIGHT_PARENTHESIS) + return +} + +func (self *_parser) parseCallExpression(left ast.Expression) ast.Expression { + argumentList, idx0, idx1 := self.parseArgumentList() + return &ast.CallExpression{ + Callee: left, + LeftParenthesis: idx0, + ArgumentList: argumentList, + RightParenthesis: idx1, + } +} + +func (self *_parser) parseDotMember(left ast.Expression) ast.Expression { + period := self.idx + self.next() + + literal := self.parsedLiteral + idx := self.idx + + if self.token == token.PRIVATE_IDENTIFIER { + self.next() + return &ast.PrivateDotExpression{ + Left: left, + Identifier: ast.PrivateIdentifier{ + Identifier: ast.Identifier{ + Idx: idx, + Name: literal, + }, + }, + } + } + + if !token.IsId(self.token) { + self.expect(token.IDENTIFIER) + self.nextStatement() + return &ast.BadExpression{From: period, To: self.idx} + } + + self.next() + + return &ast.DotExpression{ + Left: left, + Identifier: ast.Identifier{ + Idx: idx, + Name: literal, + }, + } +} + +func (self *_parser) parseBracketMember(left ast.Expression) ast.Expression { + idx0 := self.expect(token.LEFT_BRACKET) + member := self.parseExpression() + idx1 := self.expect(token.RIGHT_BRACKET) + return &ast.BracketExpression{ + LeftBracket: idx0, + Left: left, + Member: member, + RightBracket: idx1, + } +} + +func (self *_parser) parseNewExpression() ast.Expression { + idx := self.expect(token.NEW) + if self.token == token.PERIOD { + self.next() + if self.literal == "target" { + return &ast.MetaProperty{ + Meta: &ast.Identifier{ + Name: unistring.String(token.NEW.String()), + Idx: idx, + }, + Property: self.parseIdentifier(), + } + } + self.errorUnexpectedToken(token.IDENTIFIER) + } + callee := self.parseLeftHandSideExpression() + if bad, ok := callee.(*ast.BadExpression); ok { + bad.From = idx + return bad + } + node := &ast.NewExpression{ + New: idx, + Callee: callee, + } + if self.token == token.LEFT_PARENTHESIS { + argumentList, idx0, idx1 := self.parseArgumentList() + node.ArgumentList = argumentList + node.LeftParenthesis = idx0 + node.RightParenthesis = idx1 + } + return node +} + +func (self *_parser) parseLeftHandSideExpression() ast.Expression { + + var left ast.Expression + if self.token == token.NEW { + left = self.parseNewExpression() + } else { + left = self.parsePrimaryExpression() + } +L: + for { + switch self.token { + case token.PERIOD: + left = self.parseDotMember(left) + case token.LEFT_BRACKET: + left = self.parseBracketMember(left) + case token.BACKTICK: + left = self.parseTaggedTemplateLiteral(left) + default: + break L + } + } + + return left +} + +func (self *_parser) parseLeftHandSideExpressionAllowCall() ast.Expression { + + allowIn := self.scope.allowIn + self.scope.allowIn = true + defer func() { + self.scope.allowIn = allowIn + }() + + var left ast.Expression + start := self.idx + if self.token == token.NEW { + left = self.parseNewExpression() + } else { + left = self.parsePrimaryExpression() + } + + optionalChain := false +L: + for { + switch self.token { + case token.PERIOD: + left = self.parseDotMember(left) + case token.LEFT_BRACKET: + left = self.parseBracketMember(left) + case token.LEFT_PARENTHESIS: + left = self.parseCallExpression(left) + case token.BACKTICK: + if optionalChain { + self.error(self.idx, "Invalid template literal on optional chain") + self.nextStatement() + return &ast.BadExpression{From: start, To: self.idx} + } + left = self.parseTaggedTemplateLiteral(left) + case token.QUESTION_DOT: + optionalChain = true + left = &ast.Optional{Expression: left} + + switch self.peek() { + case token.LEFT_BRACKET, token.LEFT_PARENTHESIS, token.BACKTICK: + self.next() + default: + left = self.parseDotMember(left) + } + default: + break L + } + } + + if optionalChain { + left = &ast.OptionalChain{Expression: left} + } + return left +} + +func (self *_parser) parseUpdateExpression() ast.Expression { + switch self.token { + case token.INCREMENT, token.DECREMENT: + tkn := self.token + idx := self.idx + self.next() + operand := self.parseUnaryExpression() + switch operand.(type) { + case *ast.Identifier, *ast.DotExpression, *ast.PrivateDotExpression, *ast.BracketExpression: + default: + self.error(idx, "Invalid left-hand side in assignment") + self.nextStatement() + return &ast.BadExpression{From: idx, To: self.idx} + } + return &ast.UnaryExpression{ + Operator: tkn, + Idx: idx, + Operand: operand, + } + default: + operand := self.parseLeftHandSideExpressionAllowCall() + if self.token == token.INCREMENT || self.token == token.DECREMENT { + // Make sure there is no line terminator here + if self.implicitSemicolon { + return operand + } + tkn := self.token + idx := self.idx + self.next() + switch operand.(type) { + case *ast.Identifier, *ast.DotExpression, *ast.PrivateDotExpression, *ast.BracketExpression: + default: + self.error(idx, "Invalid left-hand side in assignment") + self.nextStatement() + return &ast.BadExpression{From: idx, To: self.idx} + } + return &ast.UnaryExpression{ + Operator: tkn, + Idx: idx, + Operand: operand, + Postfix: true, + } + } + return operand + } +} + +func (self *_parser) parseUnaryExpression() ast.Expression { + + switch self.token { + case token.PLUS, token.MINUS, token.NOT, token.BITWISE_NOT: + fallthrough + case token.DELETE, token.VOID, token.TYPEOF: + tkn := self.token + idx := self.idx + self.next() + return &ast.UnaryExpression{ + Operator: tkn, + Idx: idx, + Operand: self.parseUnaryExpression(), + } + case token.AWAIT: + if self.scope.allowAwait { + idx := self.idx + self.next() + if !self.scope.inAsync { + self.errorUnexpectedToken(token.AWAIT) + return &ast.BadExpression{ + From: idx, + To: self.idx, + } + } + if self.scope.inFuncParams { + self.error(idx, "Illegal await-expression in formal parameters of async function") + } + return &ast.AwaitExpression{ + Await: idx, + Argument: self.parseUnaryExpression(), + } + } + } + + return self.parseUpdateExpression() +} + +func (self *_parser) parseExponentiationExpression() ast.Expression { + parenthesis := self.token == token.LEFT_PARENTHESIS + + left := self.parseUnaryExpression() + + if self.token == token.EXPONENT { + if !parenthesis { + if u, isUnary := left.(*ast.UnaryExpression); isUnary && u.Operator != token.INCREMENT && u.Operator != token.DECREMENT { + self.error(self.idx, "Unary operator used immediately before exponentiation expression. Parenthesis must be used to disambiguate operator precedence") + } + } + for { + self.next() + left = &ast.BinaryExpression{ + Operator: token.EXPONENT, + Left: left, + Right: self.parseExponentiationExpression(), + } + if self.token != token.EXPONENT { + break + } + } + } + + return left +} + +func (self *_parser) parseMultiplicativeExpression() ast.Expression { + left := self.parseExponentiationExpression() + + for self.token == token.MULTIPLY || self.token == token.SLASH || + self.token == token.REMAINDER { + tkn := self.token + self.next() + left = &ast.BinaryExpression{ + Operator: tkn, + Left: left, + Right: self.parseExponentiationExpression(), + } + } + + return left +} + +func (self *_parser) parseAdditiveExpression() ast.Expression { + left := self.parseMultiplicativeExpression() + + for self.token == token.PLUS || self.token == token.MINUS { + tkn := self.token + self.next() + left = &ast.BinaryExpression{ + Operator: tkn, + Left: left, + Right: self.parseMultiplicativeExpression(), + } + } + + return left +} + +func (self *_parser) parseShiftExpression() ast.Expression { + left := self.parseAdditiveExpression() + + for self.token == token.SHIFT_LEFT || self.token == token.SHIFT_RIGHT || + self.token == token.UNSIGNED_SHIFT_RIGHT { + tkn := self.token + self.next() + left = &ast.BinaryExpression{ + Operator: tkn, + Left: left, + Right: self.parseAdditiveExpression(), + } + } + + return left +} + +func (self *_parser) parseRelationalExpression() ast.Expression { + if self.scope.allowIn && self.token == token.PRIVATE_IDENTIFIER { + left := &ast.PrivateIdentifier{ + Identifier: ast.Identifier{ + Idx: self.idx, + Name: self.parsedLiteral, + }, + } + self.next() + if self.token == token.IN { + self.next() + return &ast.BinaryExpression{ + Operator: self.token, + Left: left, + Right: self.parseShiftExpression(), + } + } + return left + } + left := self.parseShiftExpression() + + allowIn := self.scope.allowIn + self.scope.allowIn = true + defer func() { + self.scope.allowIn = allowIn + }() + + switch self.token { + case token.LESS, token.LESS_OR_EQUAL, token.GREATER, token.GREATER_OR_EQUAL: + tkn := self.token + self.next() + return &ast.BinaryExpression{ + Operator: tkn, + Left: left, + Right: self.parseRelationalExpression(), + Comparison: true, + } + case token.INSTANCEOF: + tkn := self.token + self.next() + return &ast.BinaryExpression{ + Operator: tkn, + Left: left, + Right: self.parseRelationalExpression(), + } + case token.IN: + if !allowIn { + return left + } + tkn := self.token + self.next() + return &ast.BinaryExpression{ + Operator: tkn, + Left: left, + Right: self.parseRelationalExpression(), + } + } + + return left +} + +func (self *_parser) parseEqualityExpression() ast.Expression { + left := self.parseRelationalExpression() + + for self.token == token.EQUAL || self.token == token.NOT_EQUAL || + self.token == token.STRICT_EQUAL || self.token == token.STRICT_NOT_EQUAL { + tkn := self.token + self.next() + left = &ast.BinaryExpression{ + Operator: tkn, + Left: left, + Right: self.parseRelationalExpression(), + Comparison: true, + } + } + + return left +} + +func (self *_parser) parseBitwiseAndExpression() ast.Expression { + left := self.parseEqualityExpression() + + for self.token == token.AND { + tkn := self.token + self.next() + left = &ast.BinaryExpression{ + Operator: tkn, + Left: left, + Right: self.parseEqualityExpression(), + } + } + + return left +} + +func (self *_parser) parseBitwiseExclusiveOrExpression() ast.Expression { + left := self.parseBitwiseAndExpression() + + for self.token == token.EXCLUSIVE_OR { + tkn := self.token + self.next() + left = &ast.BinaryExpression{ + Operator: tkn, + Left: left, + Right: self.parseBitwiseAndExpression(), + } + } + + return left +} + +func (self *_parser) parseBitwiseOrExpression() ast.Expression { + left := self.parseBitwiseExclusiveOrExpression() + + for self.token == token.OR { + tkn := self.token + self.next() + left = &ast.BinaryExpression{ + Operator: tkn, + Left: left, + Right: self.parseBitwiseExclusiveOrExpression(), + } + } + + return left +} + +func (self *_parser) parseLogicalAndExpression() ast.Expression { + left := self.parseBitwiseOrExpression() + + for self.token == token.LOGICAL_AND { + tkn := self.token + self.next() + left = &ast.BinaryExpression{ + Operator: tkn, + Left: left, + Right: self.parseBitwiseOrExpression(), + } + } + + return left +} + +func isLogicalAndExpr(expr ast.Expression) bool { + if bexp, ok := expr.(*ast.BinaryExpression); ok && bexp.Operator == token.LOGICAL_AND { + return true + } + return false +} + +func (self *_parser) parseLogicalOrExpression() ast.Expression { + var idx file.Idx + parenthesis := self.token == token.LEFT_PARENTHESIS + left := self.parseLogicalAndExpression() + + if self.token == token.LOGICAL_OR || !parenthesis && isLogicalAndExpr(left) { + for { + switch self.token { + case token.LOGICAL_OR: + self.next() + left = &ast.BinaryExpression{ + Operator: token.LOGICAL_OR, + Left: left, + Right: self.parseLogicalAndExpression(), + } + case token.COALESCE: + idx = self.idx + goto mixed + default: + return left + } + } + } else { + for { + switch self.token { + case token.COALESCE: + idx = self.idx + self.next() + + parenthesis := self.token == token.LEFT_PARENTHESIS + right := self.parseLogicalAndExpression() + if !parenthesis && isLogicalAndExpr(right) { + goto mixed + } + + left = &ast.BinaryExpression{ + Operator: token.COALESCE, + Left: left, + Right: right, + } + case token.LOGICAL_OR: + idx = self.idx + goto mixed + default: + return left + } + } + } + +mixed: + self.error(idx, "Logical expressions and coalesce expressions cannot be mixed. Wrap either by parentheses") + return left +} + +func (self *_parser) parseConditionalExpression() ast.Expression { + left := self.parseLogicalOrExpression() + + if self.token == token.QUESTION_MARK { + self.next() + allowIn := self.scope.allowIn + self.scope.allowIn = true + consequent := self.parseAssignmentExpression() + self.scope.allowIn = allowIn + self.expect(token.COLON) + return &ast.ConditionalExpression{ + Test: left, + Consequent: consequent, + Alternate: self.parseAssignmentExpression(), + } + } + + return left +} + +func (self *_parser) parseArrowFunction(start file.Idx, paramList *ast.ParameterList, async bool) ast.Expression { + self.expect(token.ARROW) + node := &ast.ArrowFunctionLiteral{ + Start: start, + ParameterList: paramList, + Async: async, + } + node.Body, node.DeclarationList = self.parseArrowFunctionBody(async) + node.Source = self.slice(start, node.Body.Idx1()) + return node +} + +func (self *_parser) parseSingleArgArrowFunction(start file.Idx, async bool) ast.Expression { + if async != self.scope.allowAwait { + self.scope.allowAwait = async + defer func() { + self.scope.allowAwait = !async + }() + } + self.tokenToBindingId() + if self.token != token.IDENTIFIER { + self.errorUnexpectedToken(self.token) + self.next() + return &ast.BadExpression{ + From: start, + To: self.idx, + } + } + + id := self.parseIdentifier() + + paramList := &ast.ParameterList{ + Opening: id.Idx, + Closing: id.Idx1(), + List: []*ast.Binding{{ + Target: id, + }}, + } + + return self.parseArrowFunction(start, paramList, async) +} + +func (self *_parser) parseAssignmentExpression() ast.Expression { + start := self.idx + parenthesis := false + async := false + var state parserState + switch self.token { + case token.LEFT_PARENTHESIS: + self.mark(&state) + parenthesis = true + case token.ASYNC: + tok := self.peek() + if self.isBindingId(tok) { + // async x => ... + self.next() + return self.parseSingleArgArrowFunction(start, true) + } else if tok == token.LEFT_PARENTHESIS { + self.mark(&state) + async = true + } + case token.YIELD: + if self.scope.allowYield { + return self.parseYieldExpression() + } + fallthrough + default: + self.tokenToBindingId() + } + left := self.parseConditionalExpression() + var operator token.Token + switch self.token { + case token.ASSIGN: + operator = self.token + case token.ADD_ASSIGN: + operator = token.PLUS + case token.SUBTRACT_ASSIGN: + operator = token.MINUS + case token.MULTIPLY_ASSIGN: + operator = token.MULTIPLY + case token.EXPONENT_ASSIGN: + operator = token.EXPONENT + case token.QUOTIENT_ASSIGN: + operator = token.SLASH + case token.REMAINDER_ASSIGN: + operator = token.REMAINDER + case token.AND_ASSIGN: + operator = token.AND + case token.OR_ASSIGN: + operator = token.OR + case token.EXCLUSIVE_OR_ASSIGN: + operator = token.EXCLUSIVE_OR + case token.SHIFT_LEFT_ASSIGN: + operator = token.SHIFT_LEFT + case token.SHIFT_RIGHT_ASSIGN: + operator = token.SHIFT_RIGHT + case token.UNSIGNED_SHIFT_RIGHT_ASSIGN: + operator = token.UNSIGNED_SHIFT_RIGHT + case token.ARROW: + var paramList *ast.ParameterList + if id, ok := left.(*ast.Identifier); ok { + paramList = &ast.ParameterList{ + Opening: id.Idx, + Closing: id.Idx1() - 1, + List: []*ast.Binding{{ + Target: id, + }}, + } + } else if parenthesis { + if seq, ok := left.(*ast.SequenceExpression); ok && len(self.errors) == 0 { + paramList = self.reinterpretSequenceAsArrowFuncParams(seq.Sequence) + } else { + self.restore(&state) + paramList = self.parseFunctionParameterList() + } + } else if async { + // async (x, y) => ... + if !self.scope.allowAwait { + self.scope.allowAwait = true + defer func() { + self.scope.allowAwait = false + }() + } + if _, ok := left.(*ast.CallExpression); ok { + self.restore(&state) + self.next() // skip "async" + paramList = self.parseFunctionParameterList() + } + } + if paramList == nil { + self.error(left.Idx0(), "Malformed arrow function parameter list") + return &ast.BadExpression{From: left.Idx0(), To: left.Idx1()} + } + return self.parseArrowFunction(start, paramList, async) + } + + if operator != 0 { + idx := self.idx + self.next() + ok := false + switch l := left.(type) { + case *ast.Identifier, *ast.DotExpression, *ast.PrivateDotExpression, *ast.BracketExpression: + ok = true + case *ast.ArrayLiteral: + if !parenthesis && operator == token.ASSIGN { + left = self.reinterpretAsArrayAssignmentPattern(l) + ok = true + } + case *ast.ObjectLiteral: + if !parenthesis && operator == token.ASSIGN { + left = self.reinterpretAsObjectAssignmentPattern(l) + ok = true + } + } + if ok { + return &ast.AssignExpression{ + Left: left, + Operator: operator, + Right: self.parseAssignmentExpression(), + } + } + self.error(left.Idx0(), "Invalid left-hand side in assignment") + self.nextStatement() + return &ast.BadExpression{From: idx, To: self.idx} + } + + return left +} + +func (self *_parser) parseYieldExpression() ast.Expression { + idx := self.expect(token.YIELD) + + if self.scope.inFuncParams { + self.error(idx, "Yield expression not allowed in formal parameter") + } + + node := &ast.YieldExpression{ + Yield: idx, + } + + if !self.implicitSemicolon && self.token == token.MULTIPLY { + node.Delegate = true + self.next() + } + + if !self.implicitSemicolon && self.token != token.SEMICOLON && self.token != token.RIGHT_BRACE && self.token != token.EOF { + var state parserState + self.mark(&state) + expr := self.parseAssignmentExpression() + if _, bad := expr.(*ast.BadExpression); bad { + expr = nil + self.restore(&state) + } + node.Argument = expr + } + + return node +} + +func (self *_parser) parseExpression() ast.Expression { + left := self.parseAssignmentExpression() + + if self.token == token.COMMA { + sequence := []ast.Expression{left} + for { + if self.token != token.COMMA { + break + } + self.next() + sequence = append(sequence, self.parseAssignmentExpression()) + } + return &ast.SequenceExpression{ + Sequence: sequence, + } + } + + return left +} + +func (self *_parser) checkComma(from, to file.Idx) { + if pos := strings.IndexByte(self.str[int(from)-self.base:int(to)-self.base], ','); pos >= 0 { + self.error(from+file.Idx(pos), "Comma is not allowed here") + } +} + +func (self *_parser) reinterpretAsArrayAssignmentPattern(left *ast.ArrayLiteral) ast.Expression { + value := left.Value + var rest ast.Expression + for i, item := range value { + if spread, ok := item.(*ast.SpreadElement); ok { + if i != len(value)-1 { + self.error(item.Idx0(), "Rest element must be last element") + return &ast.BadExpression{From: left.Idx0(), To: left.Idx1()} + } + self.checkComma(spread.Expression.Idx1(), left.RightBracket) + rest = self.reinterpretAsDestructAssignTarget(spread.Expression) + value = value[:len(value)-1] + } else { + value[i] = self.reinterpretAsAssignmentElement(item) + } + } + return &ast.ArrayPattern{ + LeftBracket: left.LeftBracket, + RightBracket: left.RightBracket, + Elements: value, + Rest: rest, + } +} + +func (self *_parser) reinterpretArrayAssignPatternAsBinding(pattern *ast.ArrayPattern) *ast.ArrayPattern { + for i, item := range pattern.Elements { + pattern.Elements[i] = self.reinterpretAsDestructBindingTarget(item) + } + if pattern.Rest != nil { + pattern.Rest = self.reinterpretAsDestructBindingTarget(pattern.Rest) + } + return pattern +} + +func (self *_parser) reinterpretAsArrayBindingPattern(left *ast.ArrayLiteral) ast.BindingTarget { + value := left.Value + var rest ast.Expression + for i, item := range value { + if spread, ok := item.(*ast.SpreadElement); ok { + if i != len(value)-1 { + self.error(item.Idx0(), "Rest element must be last element") + return &ast.BadExpression{From: left.Idx0(), To: left.Idx1()} + } + self.checkComma(spread.Expression.Idx1(), left.RightBracket) + rest = self.reinterpretAsDestructBindingTarget(spread.Expression) + value = value[:len(value)-1] + } else { + value[i] = self.reinterpretAsBindingElement(item) + } + } + return &ast.ArrayPattern{ + LeftBracket: left.LeftBracket, + RightBracket: left.RightBracket, + Elements: value, + Rest: rest, + } +} + +func (self *_parser) parseArrayBindingPattern() ast.BindingTarget { + return self.reinterpretAsArrayBindingPattern(self.parseArrayLiteral()) +} + +func (self *_parser) parseObjectBindingPattern() ast.BindingTarget { + return self.reinterpretAsObjectBindingPattern(self.parseObjectLiteral()) +} + +func (self *_parser) reinterpretArrayObjectPatternAsBinding(pattern *ast.ObjectPattern) *ast.ObjectPattern { + for _, prop := range pattern.Properties { + if keyed, ok := prop.(*ast.PropertyKeyed); ok { + keyed.Value = self.reinterpretAsBindingElement(keyed.Value) + } + } + if pattern.Rest != nil { + pattern.Rest = self.reinterpretAsBindingRestElement(pattern.Rest) + } + return pattern +} + +func (self *_parser) reinterpretAsObjectBindingPattern(expr *ast.ObjectLiteral) ast.BindingTarget { + var rest ast.Expression + value := expr.Value + for i, prop := range value { + ok := false + switch prop := prop.(type) { + case *ast.PropertyKeyed: + if prop.Kind == ast.PropertyKindValue { + prop.Value = self.reinterpretAsBindingElement(prop.Value) + ok = true + } + case *ast.PropertyShort: + ok = true + case *ast.SpreadElement: + if i != len(expr.Value)-1 { + self.error(prop.Idx0(), "Rest element must be last element") + return &ast.BadExpression{From: expr.Idx0(), To: expr.Idx1()} + } + // TODO make sure there is no trailing comma + rest = self.reinterpretAsBindingRestElement(prop.Expression) + value = value[:i] + ok = true + } + if !ok { + self.error(prop.Idx0(), "Invalid destructuring binding target") + return &ast.BadExpression{From: expr.Idx0(), To: expr.Idx1()} + } + } + return &ast.ObjectPattern{ + LeftBrace: expr.LeftBrace, + RightBrace: expr.RightBrace, + Properties: value, + Rest: rest, + } +} + +func (self *_parser) reinterpretAsObjectAssignmentPattern(l *ast.ObjectLiteral) ast.Expression { + var rest ast.Expression + value := l.Value + for i, prop := range value { + ok := false + switch prop := prop.(type) { + case *ast.PropertyKeyed: + if prop.Kind == ast.PropertyKindValue { + prop.Value = self.reinterpretAsAssignmentElement(prop.Value) + ok = true + } + case *ast.PropertyShort: + ok = true + case *ast.SpreadElement: + if i != len(l.Value)-1 { + self.error(prop.Idx0(), "Rest element must be last element") + return &ast.BadExpression{From: l.Idx0(), To: l.Idx1()} + } + // TODO make sure there is no trailing comma + rest = prop.Expression + value = value[:i] + ok = true + } + if !ok { + self.error(prop.Idx0(), "Invalid destructuring assignment target") + return &ast.BadExpression{From: l.Idx0(), To: l.Idx1()} + } + } + return &ast.ObjectPattern{ + LeftBrace: l.LeftBrace, + RightBrace: l.RightBrace, + Properties: value, + Rest: rest, + } +} + +func (self *_parser) reinterpretAsAssignmentElement(expr ast.Expression) ast.Expression { + switch expr := expr.(type) { + case *ast.AssignExpression: + if expr.Operator == token.ASSIGN { + expr.Left = self.reinterpretAsDestructAssignTarget(expr.Left) + return expr + } else { + self.error(expr.Idx0(), "Invalid destructuring assignment target") + return &ast.BadExpression{From: expr.Idx0(), To: expr.Idx1()} + } + default: + return self.reinterpretAsDestructAssignTarget(expr) + } +} + +func (self *_parser) reinterpretAsBindingElement(expr ast.Expression) ast.Expression { + switch expr := expr.(type) { + case *ast.AssignExpression: + if expr.Operator == token.ASSIGN { + expr.Left = self.reinterpretAsDestructBindingTarget(expr.Left) + return expr + } else { + self.error(expr.Idx0(), "Invalid destructuring assignment target") + return &ast.BadExpression{From: expr.Idx0(), To: expr.Idx1()} + } + default: + return self.reinterpretAsDestructBindingTarget(expr) + } +} + +func (self *_parser) reinterpretAsBinding(expr ast.Expression) *ast.Binding { + switch expr := expr.(type) { + case *ast.AssignExpression: + if expr.Operator == token.ASSIGN { + return &ast.Binding{ + Target: self.reinterpretAsDestructBindingTarget(expr.Left), + Initializer: expr.Right, + } + } else { + self.error(expr.Idx0(), "Invalid destructuring assignment target") + return &ast.Binding{ + Target: &ast.BadExpression{From: expr.Idx0(), To: expr.Idx1()}, + } + } + default: + return &ast.Binding{ + Target: self.reinterpretAsDestructBindingTarget(expr), + } + } +} + +func (self *_parser) reinterpretAsDestructAssignTarget(item ast.Expression) ast.Expression { + switch item := item.(type) { + case nil: + return nil + case *ast.ArrayLiteral: + return self.reinterpretAsArrayAssignmentPattern(item) + case *ast.ObjectLiteral: + return self.reinterpretAsObjectAssignmentPattern(item) + case ast.Pattern, *ast.Identifier, *ast.DotExpression, *ast.PrivateDotExpression, *ast.BracketExpression: + return item + } + self.error(item.Idx0(), "Invalid destructuring assignment target") + return &ast.BadExpression{From: item.Idx0(), To: item.Idx1()} +} + +func (self *_parser) reinterpretAsDestructBindingTarget(item ast.Expression) ast.BindingTarget { + switch item := item.(type) { + case nil: + return nil + case *ast.ArrayPattern: + return self.reinterpretArrayAssignPatternAsBinding(item) + case *ast.ObjectPattern: + return self.reinterpretArrayObjectPatternAsBinding(item) + case *ast.ArrayLiteral: + return self.reinterpretAsArrayBindingPattern(item) + case *ast.ObjectLiteral: + return self.reinterpretAsObjectBindingPattern(item) + case *ast.Identifier: + if !self.scope.allowAwait || item.Name != "await" { + return item + } + } + self.error(item.Idx0(), "Invalid destructuring binding target") + return &ast.BadExpression{From: item.Idx0(), To: item.Idx1()} +} + +func (self *_parser) reinterpretAsBindingRestElement(expr ast.Expression) ast.Expression { + if _, ok := expr.(*ast.Identifier); ok { + return expr + } + self.error(expr.Idx0(), "Invalid binding rest") + return &ast.BadExpression{From: expr.Idx0(), To: expr.Idx1()} +} diff --git a/goja/parser/lexer.go b/goja/parser/lexer.go new file mode 100644 index 0000000..7d824be --- /dev/null +++ b/goja/parser/lexer.go @@ -0,0 +1,1205 @@ +package parser + +import ( + "errors" + "fmt" + "math/big" + "strconv" + "strings" + "unicode" + "unicode/utf16" + "unicode/utf8" + + "golang.org/x/text/unicode/rangetable" + + "github.com/dop251/goja/file" + "github.com/dop251/goja/token" + "github.com/dop251/goja/unistring" +) + +var ( + unicodeRangeIdNeg = rangetable.Merge(unicode.Pattern_Syntax, unicode.Pattern_White_Space) + unicodeRangeIdStartPos = rangetable.Merge(unicode.Letter, unicode.Nl, unicode.Other_ID_Start) + unicodeRangeIdContPos = rangetable.Merge(unicodeRangeIdStartPos, unicode.Mn, unicode.Mc, unicode.Nd, unicode.Pc, unicode.Other_ID_Continue) +) + +func isDecimalDigit(chr rune) bool { + return '0' <= chr && chr <= '9' +} + +func IsIdentifier(s string) bool { + if s == "" { + return false + } + r, size := utf8.DecodeRuneInString(s) + if !isIdentifierStart(r) { + return false + } + for _, r := range s[size:] { + if !isIdentifierPart(r) { + return false + } + } + return true +} + +func digitValue(chr rune) int { + switch { + case '0' <= chr && chr <= '9': + return int(chr - '0') + case 'a' <= chr && chr <= 'f': + return int(chr - 'a' + 10) + case 'A' <= chr && chr <= 'F': + return int(chr - 'A' + 10) + } + return 16 // Larger than any legal digit value +} + +func isDigit(chr rune, base int) bool { + return digitValue(chr) < base +} + +func isIdStartUnicode(r rune) bool { + return unicode.Is(unicodeRangeIdStartPos, r) && !unicode.Is(unicodeRangeIdNeg, r) +} + +func isIdPartUnicode(r rune) bool { + return unicode.Is(unicodeRangeIdContPos, r) && !unicode.Is(unicodeRangeIdNeg, r) || r == '\u200C' || r == '\u200D' +} + +func isIdentifierStart(chr rune) bool { + return chr == '$' || chr == '_' || chr == '\\' || + 'a' <= chr && chr <= 'z' || 'A' <= chr && chr <= 'Z' || + chr >= utf8.RuneSelf && isIdStartUnicode(chr) +} + +func isIdentifierPart(chr rune) bool { + return chr == '$' || chr == '_' || chr == '\\' || + 'a' <= chr && chr <= 'z' || 'A' <= chr && chr <= 'Z' || + '0' <= chr && chr <= '9' || + chr >= utf8.RuneSelf && isIdPartUnicode(chr) +} + +func (self *_parser) scanIdentifier() (string, unistring.String, bool, string) { + offset := self.chrOffset + hasEscape := false + isUnicode := false + length := 0 + for isIdentifierPart(self.chr) { + r := self.chr + length++ + if r == '\\' { + hasEscape = true + distance := self.chrOffset - offset + self.read() + if self.chr != 'u' { + return "", "", false, fmt.Sprintf("Invalid identifier escape character: %c (%s)", self.chr, string(self.chr)) + } + var value rune + if self._peek() == '{' { + self.read() + value = -1 + for value <= utf8.MaxRune { + self.read() + if self.chr == '}' { + break + } + decimal, ok := hex2decimal(byte(self.chr)) + if !ok { + return "", "", false, "Invalid Unicode escape sequence" + } + if value == -1 { + value = decimal + } else { + value = value<<4 | decimal + } + } + if value == -1 { + return "", "", false, "Invalid Unicode escape sequence" + } + } else { + for j := 0; j < 4; j++ { + self.read() + decimal, ok := hex2decimal(byte(self.chr)) + if !ok { + return "", "", false, fmt.Sprintf("Invalid identifier escape character: %c (%s)", self.chr, string(self.chr)) + } + value = value<<4 | decimal + } + } + if value == '\\' { + return "", "", false, fmt.Sprintf("Invalid identifier escape value: %c (%s)", value, string(value)) + } else if distance == 0 { + if !isIdentifierStart(value) { + return "", "", false, fmt.Sprintf("Invalid identifier escape value: %c (%s)", value, string(value)) + } + } else if distance > 0 { + if !isIdentifierPart(value) { + return "", "", false, fmt.Sprintf("Invalid identifier escape value: %c (%s)", value, string(value)) + } + } + r = value + } + if r >= utf8.RuneSelf { + isUnicode = true + if r > 0xFFFF { + length++ + } + } + self.read() + } + + literal := self.str[offset:self.chrOffset] + var parsed unistring.String + if hasEscape || isUnicode { + var err string + // TODO strict + parsed, err = parseStringLiteral(literal, length, isUnicode, false) + if err != "" { + return "", "", false, err + } + } else { + parsed = unistring.String(literal) + } + + return literal, parsed, hasEscape, "" +} + +// 7.2 +func isLineWhiteSpace(chr rune) bool { + switch chr { + case '\u0009', '\u000b', '\u000c', '\u0020', '\u00a0', '\ufeff': + return true + case '\u000a', '\u000d', '\u2028', '\u2029': + return false + case '\u0085': + return false + } + return unicode.IsSpace(chr) +} + +// 7.3 +func isLineTerminator(chr rune) bool { + switch chr { + case '\u000a', '\u000d', '\u2028', '\u2029': + return true + } + return false +} + +type parserState struct { + idx file.Idx + tok token.Token + literal string + parsedLiteral unistring.String + implicitSemicolon, insertSemicolon bool + chr rune + chrOffset, offset int + errorCount int +} + +func (self *_parser) mark(state *parserState) *parserState { + if state == nil { + state = &parserState{} + } + state.idx, state.tok, state.literal, state.parsedLiteral, state.implicitSemicolon, state.insertSemicolon, state.chr, state.chrOffset, state.offset = + self.idx, self.token, self.literal, self.parsedLiteral, self.implicitSemicolon, self.insertSemicolon, self.chr, self.chrOffset, self.offset + + state.errorCount = len(self.errors) + return state +} + +func (self *_parser) restore(state *parserState) { + self.idx, self.token, self.literal, self.parsedLiteral, self.implicitSemicolon, self.insertSemicolon, self.chr, self.chrOffset, self.offset = + state.idx, state.tok, state.literal, state.parsedLiteral, state.implicitSemicolon, state.insertSemicolon, state.chr, state.chrOffset, state.offset + self.errors = self.errors[:state.errorCount] +} + +func (self *_parser) peek() token.Token { + implicitSemicolon, insertSemicolon, chr, chrOffset, offset := self.implicitSemicolon, self.insertSemicolon, self.chr, self.chrOffset, self.offset + tok, _, _, _ := self.scan() + self.implicitSemicolon, self.insertSemicolon, self.chr, self.chrOffset, self.offset = implicitSemicolon, insertSemicolon, chr, chrOffset, offset + return tok +} + +func (self *_parser) scan() (tkn token.Token, literal string, parsedLiteral unistring.String, idx file.Idx) { + + self.implicitSemicolon = false + + for { + self.skipWhiteSpace() + + idx = self.idxOf(self.chrOffset) + insertSemicolon := false + + switch chr := self.chr; { + case isIdentifierStart(chr): + var err string + var hasEscape bool + literal, parsedLiteral, hasEscape, err = self.scanIdentifier() + if err != "" { + tkn = token.ILLEGAL + break + } + if len(parsedLiteral) > 1 { + // Keywords are longer than 1 character, avoid lookup otherwise + var strict bool + tkn, strict = token.IsKeyword(string(parsedLiteral)) + if hasEscape { + self.insertSemicolon = true + if tkn == 0 || self.isBindingId(tkn) { + tkn = token.IDENTIFIER + } else { + tkn = token.ESCAPED_RESERVED_WORD + } + return + } + switch tkn { + case 0: // Not a keyword + // no-op + case token.KEYWORD: + if strict { + // TODO If strict and in strict mode, then this is not a break + break + } + return + + case + token.BOOLEAN, + token.NULL, + token.THIS, + token.BREAK, + token.THROW, // A newline after a throw is not allowed, but we need to detect it + token.YIELD, + token.RETURN, + token.CONTINUE, + token.DEBUGGER: + self.insertSemicolon = true + return + + case token.ASYNC: + // async only has special meaning if not followed by a LineTerminator + if self.skipWhiteSpaceCheckLineTerminator() { + self.insertSemicolon = true + tkn = token.IDENTIFIER + } + return + default: + return + + } + } + self.insertSemicolon = true + tkn = token.IDENTIFIER + return + case '0' <= chr && chr <= '9': + self.insertSemicolon = true + tkn, literal = self.scanNumericLiteral(false) + return + default: + self.read() + switch chr { + case -1: + if self.insertSemicolon { + self.insertSemicolon = false + self.implicitSemicolon = true + } + tkn = token.EOF + case '\r', '\n', '\u2028', '\u2029': + self.insertSemicolon = false + self.implicitSemicolon = true + continue + case ':': + tkn = token.COLON + case '.': + if digitValue(self.chr) < 10 { + insertSemicolon = true + tkn, literal = self.scanNumericLiteral(true) + } else { + if self.chr == '.' { + self.read() + if self.chr == '.' { + self.read() + tkn = token.ELLIPSIS + } else { + tkn = token.ILLEGAL + } + } else { + tkn = token.PERIOD + } + } + case ',': + tkn = token.COMMA + case ';': + tkn = token.SEMICOLON + case '(': + tkn = token.LEFT_PARENTHESIS + case ')': + tkn = token.RIGHT_PARENTHESIS + insertSemicolon = true + case '[': + tkn = token.LEFT_BRACKET + case ']': + tkn = token.RIGHT_BRACKET + insertSemicolon = true + case '{': + tkn = token.LEFT_BRACE + case '}': + tkn = token.RIGHT_BRACE + insertSemicolon = true + case '+': + tkn = self.switch3(token.PLUS, token.ADD_ASSIGN, '+', token.INCREMENT) + if tkn == token.INCREMENT { + insertSemicolon = true + } + case '-': + tkn = self.switch3(token.MINUS, token.SUBTRACT_ASSIGN, '-', token.DECREMENT) + if tkn == token.DECREMENT { + insertSemicolon = true + } + case '*': + if self.chr == '*' { + self.read() + tkn = self.switch2(token.EXPONENT, token.EXPONENT_ASSIGN) + } else { + tkn = self.switch2(token.MULTIPLY, token.MULTIPLY_ASSIGN) + } + case '/': + if self.chr == '/' { + self.skipSingleLineComment() + continue + } else if self.chr == '*' { + if self.skipMultiLineComment() { + self.insertSemicolon = false + self.implicitSemicolon = true + } + continue + } else { + // Could be division, could be RegExp literal + tkn = self.switch2(token.SLASH, token.QUOTIENT_ASSIGN) + insertSemicolon = true + } + case '%': + tkn = self.switch2(token.REMAINDER, token.REMAINDER_ASSIGN) + case '^': + tkn = self.switch2(token.EXCLUSIVE_OR, token.EXCLUSIVE_OR_ASSIGN) + case '<': + tkn = self.switch4(token.LESS, token.LESS_OR_EQUAL, '<', token.SHIFT_LEFT, token.SHIFT_LEFT_ASSIGN) + case '>': + tkn = self.switch6(token.GREATER, token.GREATER_OR_EQUAL, '>', token.SHIFT_RIGHT, token.SHIFT_RIGHT_ASSIGN, '>', token.UNSIGNED_SHIFT_RIGHT, token.UNSIGNED_SHIFT_RIGHT_ASSIGN) + case '=': + if self.chr == '>' { + self.read() + if self.implicitSemicolon { + tkn = token.ILLEGAL + } else { + tkn = token.ARROW + } + } else { + tkn = self.switch2(token.ASSIGN, token.EQUAL) + if tkn == token.EQUAL && self.chr == '=' { + self.read() + tkn = token.STRICT_EQUAL + } + } + case '!': + tkn = self.switch2(token.NOT, token.NOT_EQUAL) + if tkn == token.NOT_EQUAL && self.chr == '=' { + self.read() + tkn = token.STRICT_NOT_EQUAL + } + case '&': + tkn = self.switch3(token.AND, token.AND_ASSIGN, '&', token.LOGICAL_AND) + case '|': + tkn = self.switch3(token.OR, token.OR_ASSIGN, '|', token.LOGICAL_OR) + case '~': + tkn = token.BITWISE_NOT + case '?': + if self.chr == '.' && !isDecimalDigit(self._peek()) { + self.read() + tkn = token.QUESTION_DOT + } else if self.chr == '?' { + self.read() + tkn = token.COALESCE + } else { + tkn = token.QUESTION_MARK + } + case '"', '\'': + insertSemicolon = true + tkn = token.STRING + var err string + literal, parsedLiteral, err = self.scanString(self.chrOffset-1, true) + if err != "" { + tkn = token.ILLEGAL + } + case '`': + tkn = token.BACKTICK + case '#': + if self.chrOffset == 1 && self.chr == '!' { + self.skipSingleLineComment() + continue + } + + var err string + literal, parsedLiteral, _, err = self.scanIdentifier() + if err != "" || literal == "" { + tkn = token.ILLEGAL + break + } + self.insertSemicolon = true + tkn = token.PRIVATE_IDENTIFIER + return + default: + self.errorUnexpected(idx, chr) + tkn = token.ILLEGAL + } + } + self.insertSemicolon = insertSemicolon + return + } +} + +func (self *_parser) switch2(tkn0, tkn1 token.Token) token.Token { + if self.chr == '=' { + self.read() + return tkn1 + } + return tkn0 +} + +func (self *_parser) switch3(tkn0, tkn1 token.Token, chr2 rune, tkn2 token.Token) token.Token { + if self.chr == '=' { + self.read() + return tkn1 + } + if self.chr == chr2 { + self.read() + return tkn2 + } + return tkn0 +} + +func (self *_parser) switch4(tkn0, tkn1 token.Token, chr2 rune, tkn2, tkn3 token.Token) token.Token { + if self.chr == '=' { + self.read() + return tkn1 + } + if self.chr == chr2 { + self.read() + if self.chr == '=' { + self.read() + return tkn3 + } + return tkn2 + } + return tkn0 +} + +func (self *_parser) switch6(tkn0, tkn1 token.Token, chr2 rune, tkn2, tkn3 token.Token, chr3 rune, tkn4, tkn5 token.Token) token.Token { + if self.chr == '=' { + self.read() + return tkn1 + } + if self.chr == chr2 { + self.read() + if self.chr == '=' { + self.read() + return tkn3 + } + if self.chr == chr3 { + self.read() + if self.chr == '=' { + self.read() + return tkn5 + } + return tkn4 + } + return tkn2 + } + return tkn0 +} + +func (self *_parser) _peek() rune { + if self.offset < self.length { + return rune(self.str[self.offset]) + } + return -1 +} + +func (self *_parser) read() { + if self.offset < self.length { + self.chrOffset = self.offset + chr, width := rune(self.str[self.offset]), 1 + if chr >= utf8.RuneSelf { // !ASCII + chr, width = utf8.DecodeRuneInString(self.str[self.offset:]) + if chr == utf8.RuneError && width == 1 { + self.error(self.chrOffset, "Invalid UTF-8 character") + } + } + self.offset += width + self.chr = chr + } else { + self.chrOffset = self.length + self.chr = -1 // EOF + } +} + +func (self *_parser) skipSingleLineComment() { + for self.chr != -1 { + self.read() + if isLineTerminator(self.chr) { + return + } + } +} + +func (self *_parser) skipMultiLineComment() (hasLineTerminator bool) { + self.read() + for self.chr >= 0 { + chr := self.chr + if chr == '\r' || chr == '\n' || chr == '\u2028' || chr == '\u2029' { + hasLineTerminator = true + break + } + self.read() + if chr == '*' && self.chr == '/' { + self.read() + return + } + } + for self.chr >= 0 { + chr := self.chr + self.read() + if chr == '*' && self.chr == '/' { + self.read() + return + } + } + + self.errorUnexpected(0, self.chr) + return +} + +func (self *_parser) skipWhiteSpaceCheckLineTerminator() bool { + for { + switch self.chr { + case ' ', '\t', '\f', '\v', '\u00a0', '\ufeff': + self.read() + continue + case '\r': + if self._peek() == '\n' { + self.read() + } + fallthrough + case '\u2028', '\u2029', '\n': + return true + } + if self.chr >= utf8.RuneSelf { + if unicode.IsSpace(self.chr) { + self.read() + continue + } + } + break + } + return false +} + +func (self *_parser) skipWhiteSpace() { + for { + switch self.chr { + case ' ', '\t', '\f', '\v', '\u00a0', '\ufeff': + self.read() + continue + case '\r': + if self._peek() == '\n' { + self.read() + } + fallthrough + case '\u2028', '\u2029', '\n': + if self.insertSemicolon { + return + } + self.read() + continue + } + if self.chr >= utf8.RuneSelf { + if unicode.IsSpace(self.chr) { + self.read() + continue + } + } + break + } +} + +func (self *_parser) scanMantissa(base int, allowSeparator bool) { + for digitValue(self.chr) < base || (allowSeparator && self.chr == '_') { + afterUnderscore := self.chr == '_' + self.read() + if afterUnderscore && !isDigit(self.chr, base) { + self.error(self.chrOffset, "Only one underscore is allowed as numeric separator") + } + } +} + +func (self *_parser) scanEscape(quote rune) (int, bool) { + + var length, base uint32 + chr := self.chr + switch chr { + case '0', '1', '2', '3', '4', '5', '6', '7': + // Octal: + length, base = 3, 8 + case 'a', 'b', 'f', 'n', 'r', 't', 'v', '\\', '"', '\'': + self.read() + return 1, false + case '\r': + self.read() + if self.chr == '\n' { + self.read() + return 2, false + } + return 1, false + case '\n': + self.read() + return 1, false + case '\u2028', '\u2029': + self.read() + return 1, true + case 'x': + self.read() + length, base = 2, 16 + case 'u': + self.read() + if self.chr == '{' { + self.read() + length, base = 0, 16 + } else { + length, base = 4, 16 + } + default: + self.read() // Always make progress + } + + if base > 0 { + var value uint32 + if length > 0 { + for ; length > 0 && self.chr != quote && self.chr >= 0; length-- { + digit := uint32(digitValue(self.chr)) + if digit >= base { + break + } + value = value*base + digit + self.read() + } + } else { + for self.chr != quote && self.chr >= 0 && value < utf8.MaxRune { + if self.chr == '}' { + self.read() + break + } + digit := uint32(digitValue(self.chr)) + if digit >= base { + break + } + value = value*base + digit + self.read() + } + } + chr = rune(value) + } + if chr >= utf8.RuneSelf { + if chr > 0xFFFF { + return 2, true + } + return 1, true + } + return 1, false +} + +func (self *_parser) scanString(offset int, parse bool) (literal string, parsed unistring.String, err string) { + // " ' / + quote := rune(self.str[offset]) + length := 0 + isUnicode := false + for self.chr != quote { + chr := self.chr + if chr == '\n' || chr == '\r' || chr < 0 { + goto newline + } + if quote == '/' && (self.chr == '\u2028' || self.chr == '\u2029') { + goto newline + } + self.read() + if chr == '\\' { + if self.chr == '\n' || self.chr == '\r' || self.chr == '\u2028' || self.chr == '\u2029' || self.chr < 0 { + if quote == '/' { + goto newline + } + self.scanNewline() + } else { + l, u := self.scanEscape(quote) + length += l + if u { + isUnicode = true + } + } + continue + } else if chr == '[' && quote == '/' { + // Allow a slash (/) in a bracket character class ([...]) + // TODO Fix this, this is hacky... + quote = -1 + } else if chr == ']' && quote == -1 { + quote = '/' + } + if chr >= utf8.RuneSelf { + isUnicode = true + if chr > 0xFFFF { + length++ + } + } + length++ + } + + // " ' / + self.read() + literal = self.str[offset:self.chrOffset] + if parse { + // TODO strict + parsed, err = parseStringLiteral(literal[1:len(literal)-1], length, isUnicode, false) + } + return + +newline: + self.scanNewline() + errStr := "String not terminated" + if quote == '/' { + errStr = "Invalid regular expression: missing /" + self.error(self.idxOf(offset), errStr) + } + return "", "", errStr +} + +func (self *_parser) scanNewline() { + if self.chr == '\u2028' || self.chr == '\u2029' { + self.read() + return + } + if self.chr == '\r' { + self.read() + if self.chr != '\n' { + return + } + } + self.read() +} + +func (self *_parser) parseTemplateCharacters() (literal string, parsed unistring.String, finished bool, parseErr, err string) { + offset := self.chrOffset + var end int + length := 0 + isUnicode := false + hasCR := false + for { + chr := self.chr + if chr < 0 { + goto unterminated + } + self.read() + if chr == '`' { + finished = true + end = self.chrOffset - 1 + break + } + if chr == '\\' { + if self.chr == '\n' || self.chr == '\r' || self.chr == '\u2028' || self.chr == '\u2029' || self.chr < 0 { + if self.chr == '\r' { + hasCR = true + } + self.scanNewline() + } else { + if self.chr == '8' || self.chr == '9' { + if parseErr == "" { + parseErr = "\\8 and \\9 are not allowed in template strings." + } + } + l, u := self.scanEscape('`') + length += l + if u { + isUnicode = true + } + } + continue + } + if chr == '$' && self.chr == '{' { + self.read() + end = self.chrOffset - 2 + break + } + if chr >= utf8.RuneSelf { + isUnicode = true + if chr > 0xFFFF { + length++ + } + } else if chr == '\r' { + hasCR = true + if self.chr == '\n' { + length-- + } + } + length++ + } + literal = self.str[offset:end] + if hasCR { + literal = normaliseCRLF(literal) + } + if parseErr == "" { + parsed, parseErr = parseStringLiteral(literal, length, isUnicode, true) + } + self.insertSemicolon = true + return +unterminated: + err = err_UnexpectedEndOfInput + finished = true + return +} + +func normaliseCRLF(s string) string { + var buf strings.Builder + buf.Grow(len(s)) + for i := 0; i < len(s); i++ { + if s[i] == '\r' { + buf.WriteByte('\n') + if i < len(s)-1 && s[i+1] == '\n' { + i++ + } + } else { + buf.WriteByte(s[i]) + } + } + return buf.String() +} + +func hex2decimal(chr byte) (value rune, ok bool) { + { + chr := rune(chr) + switch { + case '0' <= chr && chr <= '9': + return chr - '0', true + case 'a' <= chr && chr <= 'f': + return chr - 'a' + 10, true + case 'A' <= chr && chr <= 'F': + return chr - 'A' + 10, true + } + return + } +} + +func parseNumberLiteral(literal string) (value interface{}, err error) { + // TODO Is Uint okay? What about -MAX_UINT + value, err = strconv.ParseInt(literal, 0, 64) + if err == nil { + return + } + + parseIntErr := err // Save this first error, just in case + + value, err = strconv.ParseFloat(literal, 64) + if err == nil { + return + } else if err.(*strconv.NumError).Err == strconv.ErrRange { + // Infinity, etc. + return value, nil + } + + err = parseIntErr + + if err.(*strconv.NumError).Err == strconv.ErrRange { + if len(literal) > 2 && + literal[0] == '0' && (literal[1] == 'X' || literal[1] == 'x') && + literal[len(literal)-1] != 'n' { + // Could just be a very large number (e.g. 0x8000000000000000) + var value float64 + literal = literal[2:] + for _, chr := range literal { + digit := digitValue(chr) + if digit >= 16 { + goto error + } + value = value*16 + float64(digit) + } + return value, nil + } + } + + if len(literal) > 1 && literal[len(literal)-1] == 'n' { + if literal[0] == '0' { + if len(literal) > 2 && isDecimalDigit(rune(literal[1])) { + goto error + } + } + // Parse as big.Int + bigInt := new(big.Int) + _, ok := bigInt.SetString(literal[:len(literal)-1], 0) + if !ok { + goto error + } + return bigInt, nil + } + +error: + return nil, errors.New("Illegal numeric literal") +} + +func parseStringLiteral(literal string, length int, unicode, strict bool) (unistring.String, string) { + var sb strings.Builder + var chars []uint16 + if unicode { + chars = make([]uint16, 1, length+1) + chars[0] = unistring.BOM + } else { + sb.Grow(length) + } + str := literal + for len(str) > 0 { + switch chr := str[0]; { + // We do not explicitly handle the case of the quote + // value, which can be: " ' / + // This assumes we're already passed a partially well-formed literal + case chr >= utf8.RuneSelf: + chr, size := utf8.DecodeRuneInString(str) + if chr <= 0xFFFF { + chars = append(chars, uint16(chr)) + } else { + first, second := utf16.EncodeRune(chr) + chars = append(chars, uint16(first), uint16(second)) + } + str = str[size:] + continue + case chr != '\\': + if unicode { + chars = append(chars, uint16(chr)) + } else { + sb.WriteByte(chr) + } + str = str[1:] + continue + } + + if len(str) <= 1 { + panic("len(str) <= 1") + } + chr := str[1] + var value rune + if chr >= utf8.RuneSelf { + str = str[1:] + var size int + value, size = utf8.DecodeRuneInString(str) + str = str[size:] // \ + + if value == '\u2028' || value == '\u2029' { + continue + } + } else { + str = str[2:] // \ + switch chr { + case 'b': + value = '\b' + case 'f': + value = '\f' + case 'n': + value = '\n' + case 'r': + value = '\r' + case 't': + value = '\t' + case 'v': + value = '\v' + case 'x', 'u': + size := 0 + switch chr { + case 'x': + size = 2 + case 'u': + if str == "" || str[0] != '{' { + size = 4 + } + } + if size > 0 { + if len(str) < size { + return "", fmt.Sprintf("invalid escape: \\%s: len(%q) != %d", string(chr), str, size) + } + for j := 0; j < size; j++ { + decimal, ok := hex2decimal(str[j]) + if !ok { + return "", fmt.Sprintf("invalid escape: \\%s: %q", string(chr), str[:size]) + } + value = value<<4 | decimal + } + } else { + str = str[1:] + var val rune + value = -1 + for ; size < len(str); size++ { + if str[size] == '}' { + if size == 0 { + return "", fmt.Sprintf("invalid escape: \\%s", string(chr)) + } + size++ + value = val + break + } + decimal, ok := hex2decimal(str[size]) + if !ok { + return "", fmt.Sprintf("invalid escape: \\%s: %q", string(chr), str[:size+1]) + } + val = val<<4 | decimal + if val > utf8.MaxRune { + return "", fmt.Sprintf("undefined Unicode code-point: %q", str[:size+1]) + } + } + if value == -1 { + return "", fmt.Sprintf("unterminated \\u{: %q", str) + } + } + str = str[size:] + if chr == 'x' { + break + } + if value > utf8.MaxRune { + panic("value > utf8.MaxRune") + } + case '0': + if len(str) == 0 || '0' > str[0] || str[0] > '7' { + value = 0 + break + } + fallthrough + case '1', '2', '3', '4', '5', '6', '7': + if strict { + return "", "Octal escape sequences are not allowed in this context" + } + value = rune(chr) - '0' + j := 0 + for ; j < 2; j++ { + if len(str) < j+1 { + break + } + chr := str[j] + if '0' > chr || chr > '7' { + break + } + decimal := rune(str[j]) - '0' + value = (value << 3) | decimal + } + str = str[j:] + case '\\': + value = '\\' + case '\'', '"': + value = rune(chr) + case '\r': + if len(str) > 0 { + if str[0] == '\n' { + str = str[1:] + } + } + fallthrough + case '\n': + continue + default: + value = rune(chr) + } + } + if unicode { + if value <= 0xFFFF { + chars = append(chars, uint16(value)) + } else { + first, second := utf16.EncodeRune(value) + chars = append(chars, uint16(first), uint16(second)) + } + } else { + if value >= utf8.RuneSelf { + return "", "Unexpected unicode character" + } + sb.WriteByte(byte(value)) + } + } + + if unicode { + if len(chars) != length+1 { + panic(fmt.Errorf("unexpected unicode length while parsing '%s'", literal)) + } + return unistring.FromUtf16(chars), "" + } + if sb.Len() != length { + panic(fmt.Errorf("unexpected length while parsing '%s'", literal)) + } + return unistring.String(sb.String()), "" +} + +func (self *_parser) scanNumericLiteral(decimalPoint bool) (token.Token, string) { + + offset := self.chrOffset + tkn := token.NUMBER + + if decimalPoint { + offset-- + self.scanMantissa(10, true) + } else { + if self.chr == '0' { + self.read() + base := 0 + switch self.chr { + case 'x', 'X': + base = 16 + case 'o', 'O': + base = 8 + case 'b', 'B': + base = 2 + case '.', 'e', 'E': + // no-op + default: + // legacy octal + self.scanMantissa(8, false) + goto end + } + if base > 0 { + self.read() + if !isDigit(self.chr, base) { + return token.ILLEGAL, self.str[offset:self.chrOffset] + } + self.scanMantissa(base, true) + goto end + } + } else { + self.scanMantissa(10, true) + } + if self.chr == '.' { + self.read() + self.scanMantissa(10, true) + } + } + + if self.chr == 'e' || self.chr == 'E' { + self.read() + if self.chr == '-' || self.chr == '+' { + self.read() + } + if isDecimalDigit(self.chr) { + self.read() + self.scanMantissa(10, true) + } else { + return token.ILLEGAL, self.str[offset:self.chrOffset] + } + } +end: + if self.chr == 'n' || self.chr == 'N' { + self.read() + return tkn, self.str[offset:self.chrOffset] + } + if isIdentifierStart(self.chr) || isDecimalDigit(self.chr) { + return token.ILLEGAL, self.str[offset:self.chrOffset] + } + + return tkn, self.str[offset:self.chrOffset] +} diff --git a/goja/parser/lexer_test.go b/goja/parser/lexer_test.go new file mode 100644 index 0000000..0eefa40 --- /dev/null +++ b/goja/parser/lexer_test.go @@ -0,0 +1,423 @@ +package parser + +import ( + "testing" + + "github.com/dop251/goja/file" + "github.com/dop251/goja/token" + "github.com/dop251/goja/unistring" +) + +func TestLexer(t *testing.T) { + tt(t, func() { + setup := func(src string) *_parser { + parser := newParser("", src) + return parser + } + + test := func(src string, test ...interface{}) { + parser := setup(src) + for len(test) > 0 { + tkn, literal, _, idx := parser.scan() + if len(test) > 0 { + is(tkn, test[0].(token.Token)) + test = test[1:] + } + if len(test) > 0 { + is(literal, unistring.String(test[0].(string))) + test = test[1:] + } + if len(test) > 0 { + // FIXME terst, Fix this so that cast to file.Idx is not necessary? + is(idx, file.Idx(test[0].(int))) + test = test[1:] + } + } + } + + test("", + token.EOF, "", 1, + ) + + test("#!", + token.EOF, "", 3, + ) + + test("#!\n1", + token.NUMBER, "1", 4, + token.EOF, "", 5, + ) + + test("1", + token.NUMBER, "1", 1, + token.EOF, "", 2, + ) + + test(".0", + token.NUMBER, ".0", 1, + token.EOF, "", 3, + ) + + test("abc", + token.IDENTIFIER, "abc", 1, + token.EOF, "", 4, + ) + + test("abc(1)", + token.IDENTIFIER, "abc", 1, + token.LEFT_PARENTHESIS, "", 4, + token.NUMBER, "1", 5, + token.RIGHT_PARENTHESIS, "", 6, + token.EOF, "", 7, + ) + + test(".", + token.PERIOD, "", 1, + token.EOF, "", 2, + ) + + test("===.", + token.STRICT_EQUAL, "", 1, + token.PERIOD, "", 4, + token.EOF, "", 5, + ) + + test(">>>=.0", + token.UNSIGNED_SHIFT_RIGHT_ASSIGN, "", 1, + token.NUMBER, ".0", 5, + token.EOF, "", 7, + ) + + test(">>>=0.0.", + token.UNSIGNED_SHIFT_RIGHT_ASSIGN, "", 1, + token.NUMBER, "0.0", 5, + token.PERIOD, "", 8, + token.EOF, "", 9, + ) + + test("\"abc\"", + token.STRING, "\"abc\"", 1, + token.EOF, "", 6, + ) + + test("abc = //", + token.IDENTIFIER, "abc", 1, + token.ASSIGN, "", 5, + token.EOF, "", 9, + ) + + test("abc = 1 / 2", + token.IDENTIFIER, "abc", 1, + token.ASSIGN, "", 5, + token.NUMBER, "1", 7, + token.SLASH, "", 9, + token.NUMBER, "2", 11, + token.EOF, "", 12, + ) + + test("xyzzy = 'Nothing happens.'", + token.IDENTIFIER, "xyzzy", 1, + token.ASSIGN, "", 7, + token.STRING, "'Nothing happens.'", 9, + token.EOF, "", 27, + ) + + test("abc = !false", + token.IDENTIFIER, "abc", 1, + token.ASSIGN, "", 5, + token.NOT, "", 7, + token.BOOLEAN, "false", 8, + token.EOF, "", 13, + ) + + test("abc = !!true", + token.IDENTIFIER, "abc", 1, + token.ASSIGN, "", 5, + token.NOT, "", 7, + token.NOT, "", 8, + token.BOOLEAN, "true", 9, + token.EOF, "", 13, + ) + + test("abc *= 1", + token.IDENTIFIER, "abc", 1, + token.MULTIPLY_ASSIGN, "", 5, + token.NUMBER, "1", 8, + token.EOF, "", 9, + ) + + test("if 1 else", + token.IF, "if", 1, + token.NUMBER, "1", 4, + token.ELSE, "else", 6, + token.EOF, "", 10, + ) + + test("null", + token.NULL, "null", 1, + token.EOF, "", 5, + ) + + test(`"\u007a\x79\u000a\x78"`, + token.STRING, "\"\\u007a\\x79\\u000a\\x78\"", 1, + token.EOF, "", 23, + ) + + test(`"[First line \ +Second line \ + Third line\ +. ]" + `, + token.STRING, "\"[First line \\\nSecond line \\\n Third line\\\n. ]\"", 1, + token.EOF, "", 53, + ) + + test("/", + token.SLASH, "", 1, + token.EOF, "", 2, + ) + + test("var abc = \"abc\uFFFFabc\"", + token.VAR, "var", 1, + token.IDENTIFIER, "abc", 5, + token.ASSIGN, "", 9, + token.STRING, "\"abc\uFFFFabc\"", 11, + token.EOF, "", 22, + ) + + test(`'\t' === '\r'`, + token.STRING, "'\\t'", 1, + token.STRICT_EQUAL, "", 6, + token.STRING, "'\\r'", 10, + token.EOF, "", 14, + ) + + test(`var \u0024 = 1`, + token.VAR, "var", 1, + token.IDENTIFIER, "\\u0024", 5, + token.ASSIGN, "", 12, + token.NUMBER, "1", 14, + token.EOF, "", 15, + ) + + test("10e10000", + token.NUMBER, "10e10000", 1, + token.EOF, "", 9, + ) + + test(`var if var class`, + token.VAR, "var", 1, + token.IF, "if", 5, + token.VAR, "var", 8, + token.CLASS, "class", 12, + token.EOF, "", 17, + ) + + test(`-0`, + token.MINUS, "", 1, + token.NUMBER, "0", 2, + token.EOF, "", 3, + ) + + test(`.01`, + token.NUMBER, ".01", 1, + token.EOF, "", 4, + ) + + test(`.01e+2`, + token.NUMBER, ".01e+2", 1, + token.EOF, "", 7, + ) + + test(";", + token.SEMICOLON, "", 1, + token.EOF, "", 2, + ) + + test(";;", + token.SEMICOLON, "", 1, + token.SEMICOLON, "", 2, + token.EOF, "", 3, + ) + + test("//", + token.EOF, "", 3, + ) + + test(";;//", + token.SEMICOLON, "", 1, + token.SEMICOLON, "", 2, + token.EOF, "", 5, + ) + + test("1", + token.NUMBER, "1", 1, + ) + + test("12 123", + token.NUMBER, "12", 1, + token.NUMBER, "123", 4, + ) + + test("1.2 12.3", + token.NUMBER, "1.2", 1, + token.NUMBER, "12.3", 5, + ) + + test("1_000 1_000_000", + token.NUMBER, "1_000", 1, + token.NUMBER, "1_000_000", 7, + ) + + test(`1n`, + token.NUMBER, "1n", 1, + ) + + test(`1n 9007199254740991n`, + token.NUMBER, "1n", 1, + token.NUMBER, "9007199254740991n", 4, + ) + + test(`0xabn`, + token.NUMBER, "0xabn", 1, + ) + + test(`0xabcdef0123456789abcdef0123n`, + token.NUMBER, "0xabcdef0123456789abcdef0123n", 1, + ) + + test("/ /=", + token.SLASH, "", 1, + token.QUOTIENT_ASSIGN, "", 3, + ) + + test(`"abc"`, + token.STRING, `"abc"`, 1, + ) + + test(`'abc'`, + token.STRING, `'abc'`, 1, + ) + + test("++", + token.INCREMENT, "", 1, + ) + + test(">", + token.GREATER, "", 1, + ) + + test(">=", + token.GREATER_OR_EQUAL, "", 1, + ) + + test(">>", + token.SHIFT_RIGHT, "", 1, + ) + + test(">>=", + token.SHIFT_RIGHT_ASSIGN, "", 1, + ) + + test(">>>", + token.UNSIGNED_SHIFT_RIGHT, "", 1, + ) + + test(">>>=", + token.UNSIGNED_SHIFT_RIGHT_ASSIGN, "", 1, + ) + + test("1 \"abc\"", + token.NUMBER, "1", 1, + token.STRING, "\"abc\"", 3, + ) + + test(",", + token.COMMA, "", 1, + ) + + test("1, \"abc\"", + token.NUMBER, "1", 1, + token.COMMA, "", 2, + token.STRING, "\"abc\"", 4, + ) + + test("new abc(1, 3.14159);", + token.NEW, "new", 1, + token.IDENTIFIER, "abc", 5, + token.LEFT_PARENTHESIS, "", 8, + token.NUMBER, "1", 9, + token.COMMA, "", 10, + token.NUMBER, "3.14159", 12, + token.RIGHT_PARENTHESIS, "", 19, + token.SEMICOLON, "", 20, + ) + + test("1 == \"1\"", + token.NUMBER, "1", 1, + token.EQUAL, "", 3, + token.STRING, "\"1\"", 6, + ) + + test("1\n[]\n", + token.NUMBER, "1", 1, + token.LEFT_BRACKET, "", 3, + token.RIGHT_BRACKET, "", 4, + ) + + test("1\ufeff[]\ufeff", + token.NUMBER, "1", 1, + token.LEFT_BRACKET, "", 5, + token.RIGHT_BRACKET, "", 6, + ) + + test("x ?.30 : false", + token.IDENTIFIER, "x", 1, + token.QUESTION_MARK, "", 3, + token.NUMBER, ".30", 4, + token.COLON, "", 8, + token.BOOLEAN, "false", 10, + ) + + test("a\n?.b", + token.IDENTIFIER, "a", 1, + token.QUESTION_DOT, "", 3, + token.IDENTIFIER, "b", 5, + ) + + // ILLEGAL + + test(`3ea`, + token.ILLEGAL, "3e", 1, + token.IDENTIFIER, "a", 3, + token.EOF, "", 4, + ) + + test(`3in`, + token.ILLEGAL, "3", 1, + token.IN, "in", 2, + token.EOF, "", 4, + ) + + test("\"Hello\nWorld\"", + token.ILLEGAL, "", 1, + token.IDENTIFIER, "World", 8, + token.ILLEGAL, "", 13, + token.EOF, "", 14, + ) + + test("\u203f = 10", + token.ILLEGAL, "", 1, + token.ASSIGN, "", 5, + token.NUMBER, "10", 7, + token.EOF, "", 9, + ) + + test(`"\x0G"`, + token.ILLEGAL, "\"\\x0G\"", 1, + //token.STRING, "\"\\x0G\"", 1, + token.EOF, "", 7, + ) + + }) +} diff --git a/goja/parser/marshal_test.go b/goja/parser/marshal_test.go new file mode 100644 index 0000000..eb9a809 --- /dev/null +++ b/goja/parser/marshal_test.go @@ -0,0 +1,890 @@ +package parser + +import ( + "bytes" + "encoding/json" + "reflect" + "strings" + "testing" + + "github.com/dop251/goja/ast" +) + +func marshal(name string, children ...interface{}) interface{} { + if len(children) == 1 { + if name == "" { + return testMarshalNode(children[0]) + } + return map[string]interface{}{ + name: children[0], + } + } + map_ := map[string]interface{}{} + length := len(children) / 2 + for i := 0; i < length; i++ { + name := children[i*2].(string) + value := children[i*2+1] + map_[name] = value + } + if name == "" { + return map_ + } + return map[string]interface{}{ + name: map_, + } +} + +func testMarshalNode(node interface{}) interface{} { + switch node := node.(type) { + + // Expression + + case *ast.ArrayLiteral: + return marshal("Array", testMarshalNode(node.Value)) + + case *ast.AssignExpression: + return marshal("Assign", + "Left", testMarshalNode(node.Left), + "Right", testMarshalNode(node.Right), + ) + + case *ast.BinaryExpression: + return marshal("BinaryExpression", + "Operator", node.Operator.String(), + "Left", testMarshalNode(node.Left), + "Right", testMarshalNode(node.Right), + ) + + case *ast.BooleanLiteral: + return marshal("Literal", node.Value) + + case *ast.CallExpression: + return marshal("Call", + "Callee", testMarshalNode(node.Callee), + "ArgumentList", testMarshalNode(node.ArgumentList), + ) + + case *ast.ConditionalExpression: + return marshal("Conditional", + "Test", testMarshalNode(node.Test), + "Consequent", testMarshalNode(node.Consequent), + "Alternate", testMarshalNode(node.Alternate), + ) + + case *ast.DotExpression: + return marshal("Dot", + "Left", testMarshalNode(node.Left), + "Member", node.Identifier.Name, + ) + + case *ast.NewExpression: + return marshal("New", + "Callee", testMarshalNode(node.Callee), + "ArgumentList", testMarshalNode(node.ArgumentList), + ) + + case *ast.NullLiteral: + return marshal("Literal", nil) + + case *ast.NumberLiteral: + return marshal("Literal", node.Value) + + case *ast.ObjectLiteral: + return marshal("Object", testMarshalNode(node.Value)) + + case *ast.RegExpLiteral: + return marshal("Literal", node.Literal) + + case *ast.StringLiteral: + return marshal("Literal", node.Literal) + + case *ast.Binding: + return marshal("Binding", "Target", testMarshalNode(node.Target), + "Initializer", testMarshalNode(node.Initializer)) + + // Statement + + case *ast.Program: + return testMarshalNode(node.Body) + + case *ast.BlockStatement: + return marshal("BlockStatement", testMarshalNode(node.List)) + + case *ast.EmptyStatement: + return "EmptyStatement" + + case *ast.ExpressionStatement: + return testMarshalNode(node.Expression) + + case *ast.ForInStatement: + return marshal("ForIn", + "Into", testMarshalNode(node.Into), + "Source", marshal("", node.Source), + "Body", marshal("", node.Body), + ) + + case *ast.FunctionLiteral: + return marshal("Function", testMarshalNode(node.Body)) + + case *ast.Identifier: + return marshal("Identifier", node.Name) + + case *ast.IfStatement: + if_ := marshal("", + "Test", testMarshalNode(node.Test), + "Consequent", testMarshalNode(node.Consequent), + ).(map[string]interface{}) + if node.Alternate != nil { + if_["Alternate"] = testMarshalNode(node.Alternate) + } + return marshal("If", if_) + + case *ast.LabelledStatement: + return marshal("Label", + "Name", node.Label.Name, + "Statement", testMarshalNode(node.Statement), + ) + case *ast.PropertyKeyed: + return marshal("", + "Key", node.Key, + "Value", testMarshalNode(node.Value), + ) + + case *ast.ReturnStatement: + return marshal("Return", testMarshalNode(node.Argument)) + + case *ast.SequenceExpression: + return marshal("Sequence", testMarshalNode(node.Sequence)) + + case *ast.ThrowStatement: + return marshal("Throw", testMarshalNode(node.Argument)) + + case *ast.VariableStatement: + return marshal("Var", testMarshalNode(node.List)) + + // Special + case *ast.ForDeclaration: + return marshal("For-Into-Decl", testMarshalNode(node.Target)) + + case *ast.ForIntoVar: + return marshal("For-Into-Var", testMarshalNode(node.Binding)) + + } + + { + value := reflect.ValueOf(node) + if value.Kind() == reflect.Slice { + tmp0 := []interface{}{} + for index := 0; index < value.Len(); index++ { + tmp0 = append(tmp0, testMarshalNode(value.Index(index).Interface())) + } + return tmp0 + } + } + + return nil +} + +func testMarshal(node interface{}) string { + value, err := json.Marshal(testMarshalNode(node)) + if err != nil { + panic(err) + } + return string(value) +} + +func TestParserAST(t *testing.T) { + tt(t, func() { + + test := func(inputOutput string) { + match := matchBeforeAfterSeparator.FindStringIndex(inputOutput) + input := strings.TrimSpace(inputOutput[0:match[0]]) + wantOutput := strings.TrimSpace(inputOutput[match[1]:]) + _, program, err := testParse(input) + is(err, nil) + haveOutput := testMarshal(program) + tmp0, tmp1 := bytes.Buffer{}, bytes.Buffer{} + json.Indent(&tmp0, []byte(haveOutput), "\t\t", " ") + json.Indent(&tmp1, []byte(wantOutput), "\t\t", " ") + is("\n\t\t"+tmp0.String(), "\n\t\t"+tmp1.String()) + } + + test(` + --- +[] + `) + + test(` + ; + --- +[ + "EmptyStatement" +] + `) + + test(` + ;;; + --- +[ + "EmptyStatement", + "EmptyStatement", + "EmptyStatement" +] + `) + + test(` + 1; true; abc; "abc"; null; + --- +[ + { + "Literal": 1 + }, + { + "Literal": true + }, + { + "Identifier": "abc" + }, + { + "Literal": "\"abc\"" + }, + { + "Literal": null + } +] + `) + + test(` + { 1; null; 3.14159; ; } + --- +[ + { + "BlockStatement": [ + { + "Literal": 1 + }, + { + "Literal": null + }, + { + "Literal": 3.14159 + }, + "EmptyStatement" + ] + } +] + `) + + test(` + new abc(); + --- +[ + { + "New": { + "ArgumentList": [], + "Callee": { + "Identifier": "abc" + } + } + } +] + `) + + test(` + new abc(1, 3.14159) + --- +[ + { + "New": { + "ArgumentList": [ + { + "Literal": 1 + }, + { + "Literal": 3.14159 + } + ], + "Callee": { + "Identifier": "abc" + } + } + } +] + `) + + test(` + true ? false : true + --- +[ + { + "Conditional": { + "Alternate": { + "Literal": true + }, + "Consequent": { + "Literal": false + }, + "Test": { + "Literal": true + } + } + } +] + `) + + test(` + true || false + --- +[ + { + "BinaryExpression": { + "Left": { + "Literal": true + }, + "Operator": "||", + "Right": { + "Literal": false + } + } + } +] + `) + + test(` + 0 + { abc: true } + --- +[ + { + "BinaryExpression": { + "Left": { + "Literal": 0 + }, + "Operator": "+", + "Right": { + "Object": [ + { + "Key": { + "Idx": 7, + "Literal": "abc", + "Value": "abc" + }, + "Value": { + "Literal": true + } + } + ] + } + } + } +] + `) + + test(` + 1 == "1" + --- +[ + { + "BinaryExpression": { + "Left": { + "Literal": 1 + }, + "Operator": "==", + "Right": { + "Literal": "\"1\"" + } + } + } +] + `) + + test(` + abc(1) + --- +[ + { + "Call": { + "ArgumentList": [ + { + "Literal": 1 + } + ], + "Callee": { + "Identifier": "abc" + } + } + } +] + `) + + test(` + Math.pow(3, 2) + --- +[ + { + "Call": { + "ArgumentList": [ + { + "Literal": 3 + }, + { + "Literal": 2 + } + ], + "Callee": { + "Dot": { + "Left": { + "Identifier": "Math" + }, + "Member": "pow" + } + } + } + } +] + `) + + test(` + 1, 2, 3 + --- +[ + { + "Sequence": [ + { + "Literal": 1 + }, + { + "Literal": 2 + }, + { + "Literal": 3 + } + ] + } +] + `) + + test(` + / abc /gim; + --- +[ + { + "Literal": "/ abc /gim" + } +] + `) + + test(` + if (0) + 1; + --- +[ + { + "If": { + "Consequent": { + "Literal": 1 + }, + "Test": { + "Literal": 0 + } + } + } +] + `) + + test(` + 0+function(){ + return; + } + --- +[ + { + "BinaryExpression": { + "Left": { + "Literal": 0 + }, + "Operator": "+", + "Right": { + "Function": { + "BlockStatement": [ + { + "Return": null + } + ] + } + } + } + } +] + `) + + test(` + xyzzy // Ignore it + // Ignore this + // And this + /* And all.. + + + + ... of this! + */ + "Nothing happens." + // And finally this + --- +[ + { + "Identifier": "xyzzy" + }, + { + "Literal": "\"Nothing happens.\"" + } +] + `) + + test(` + ((x & (x = 1)) !== 0) + --- +[ + { + "BinaryExpression": { + "Left": { + "BinaryExpression": { + "Left": { + "Identifier": "x" + }, + "Operator": "\u0026", + "Right": { + "Assign": { + "Left": { + "Identifier": "x" + }, + "Right": { + "Literal": 1 + } + } + } + } + }, + "Operator": "!==", + "Right": { + "Literal": 0 + } + } + } +] + `) + + test(` + { abc: 'def' } + --- +[ + { + "BlockStatement": [ + { + "Label": { + "Name": "abc", + "Statement": { + "Literal": "'def'" + } + } + } + ] + } +] + `) + + test(` + // This is not an object, this is a string literal with a label! + ({ abc: 'def' }) + --- +[ + { + "Object": [ + { + "Key": { + "Idx": 77, + "Literal": "abc", + "Value": "abc" + }, + "Value": { + "Literal": "'def'" + } + } + ] + } +] + `) + + test(` + [,] + --- +[ + { + "Array": [ + null + ] + } +] + `) + + test(` + [,,] + --- +[ + { + "Array": [ + null, + null + ] + } +] + `) + + test(` + ({ get abc() {} }) + --- +[ + { + "Object": [ + { + "Key": { + "Idx": 8, + "Literal": "abc", + "Value": "abc" + }, + "Value": { + "Function": { + "BlockStatement": [] + } + } + } + ] + } +] + `) + + test(` + /abc/.source + --- +[ + { + "Dot": { + "Left": { + "Literal": "/abc/" + }, + "Member": "source" + } + } +] + `) + + test(` + xyzzy + + throw new TypeError("Nothing happens.") + --- +[ + { + "Identifier": "xyzzy" + }, + { + "Throw": { + "New": { + "ArgumentList": [ + { + "Literal": "\"Nothing happens.\"" + } + ], + "Callee": { + "Identifier": "TypeError" + } + } + } + } +] + `) + + // When run, this will call a type error to be thrown + // This is essentially the same as: + // + // var abc = 1(function(){})() + // + test(` + var abc = 1 + (function(){ + })() + --- +[ + { + "Var": [ + { + "Binding": { + "Initializer": { + "Call": { + "ArgumentList": [], + "Callee": { + "Call": { + "ArgumentList": [ + { + "Function": { + "BlockStatement": [] + } + } + ], + "Callee": { + "Literal": 1 + } + } + } + } + }, + "Target": { + "Identifier": "abc" + } + } + } + ] + } +] + `) + + test(` + "use strict" + --- +[ + { + "Literal": "\"use strict\"" + } +] + `) + + test(` + "use strict" + abc = 1 + 2 + 11 + --- +[ + { + "Literal": "\"use strict\"" + }, + { + "Assign": { + "Left": { + "Identifier": "abc" + }, + "Right": { + "BinaryExpression": { + "Left": { + "BinaryExpression": { + "Left": { + "Literal": 1 + }, + "Operator": "+", + "Right": { + "Literal": 2 + } + } + }, + "Operator": "+", + "Right": { + "Literal": 11 + } + } + } + } + } +] + `) + + test(` + abc = function() { 'use strict' } + --- +[ + { + "Assign": { + "Left": { + "Identifier": "abc" + }, + "Right": { + "Function": { + "BlockStatement": [ + { + "Literal": "'use strict'" + } + ] + } + } + } + } +] + `) + + test(` + for (var abc in def) { + } + --- +[ + { + "ForIn": { + "Body": { + "BlockStatement": [] + }, + "Into": { + "For-Into-Var": { + "Binding": { + "Initializer": null, + "Target": { + "Identifier": "abc" + } + } + } + }, + "Source": { + "Identifier": "def" + } + } + } +] + `) + + test(` + abc = { + '"': "'", + "'": '"', + } + --- +[ + { + "Assign": { + "Left": { + "Identifier": "abc" + }, + "Right": { + "Object": [ + { + "Key": { + "Idx": 21, + "Literal": "'\"'", + "Value": "\"" + }, + "Value": { + "Literal": "\"'\"" + } + }, + { + "Key": { + "Idx": 43, + "Literal": "\"'\"", + "Value": "'" + }, + "Value": { + "Literal": "'\"'" + } + } + ] + } + } + } +] + `) + + }) +} diff --git a/goja/parser/parser.go b/goja/parser/parser.go new file mode 100644 index 0000000..24b3802 --- /dev/null +++ b/goja/parser/parser.go @@ -0,0 +1,268 @@ +/* +Package parser implements a parser for JavaScript. + + import ( + "github.com/dop251/goja/parser" + ) + +Parse and return an AST + + filename := "" // A filename is optional + src := ` + // Sample xyzzy example + (function(){ + if (3.14159 > 0) { + console.log("Hello, World."); + return; + } + + var xyzzy = NaN; + console.log("Nothing happens."); + return xyzzy; + })(); + ` + + // Parse some JavaScript, yielding a *ast.Program and/or an ErrorList + program, err := parser.ParseFile(nil, filename, src, 0) + +# Warning + +The parser and AST interfaces are still works-in-progress (particularly where +node types are concerned) and may change in the future. +*/ +package parser + +import ( + "bytes" + "errors" + "io" + "os" + + "github.com/dop251/goja/ast" + "github.com/dop251/goja/file" + "github.com/dop251/goja/token" + "github.com/dop251/goja/unistring" +) + +// A Mode value is a set of flags (or 0). They control optional parser functionality. +type Mode uint + +const ( + IgnoreRegExpErrors Mode = 1 << iota // Ignore RegExp compatibility errors (allow backtracking) +) + +type options struct { + disableSourceMaps bool + sourceMapLoader func(path string) ([]byte, error) +} + +// Option represents one of the options for the parser to use in the Parse methods. Currently supported are: +// WithDisableSourceMaps and WithSourceMapLoader. +type Option func(*options) + +// WithDisableSourceMaps is an option to disable source maps support. May save a bit of time when source maps +// are not in use. +func WithDisableSourceMaps(opts *options) { + opts.disableSourceMaps = true +} + +// WithSourceMapLoader is an option to set a custom source map loader. The loader will be given a path or a +// URL from the sourceMappingURL. If sourceMappingURL is not absolute it is resolved relatively to the name +// of the file being parsed. Any error returned by the loader will fail the parsing. +// Note that setting this to nil does not disable source map support, there is a default loader which reads +// from the filesystem. Use WithDisableSourceMaps to disable source map support. +func WithSourceMapLoader(loader func(path string) ([]byte, error)) Option { + return func(opts *options) { + opts.sourceMapLoader = loader + } +} + +type _parser struct { + str string + length int + base int + + chr rune // The current character + chrOffset int // The offset of current character + offset int // The offset after current character (may be greater than 1) + + idx file.Idx // The index of token + token token.Token // The token + literal string // The literal of the token, if any + parsedLiteral unistring.String + + scope *_scope + insertSemicolon bool // If we see a newline, then insert an implicit semicolon + implicitSemicolon bool // An implicit semicolon exists + + errors ErrorList + + recover struct { + // Scratch when trying to seek to the next statement, etc. + idx file.Idx + count int + } + + mode Mode + opts options + + file *file.File +} + +func _newParser(filename, src string, base int, opts ...Option) *_parser { + p := &_parser{ + chr: ' ', // This is set so we can start scanning by skipping whitespace + str: src, + length: len(src), + base: base, + file: file.NewFile(filename, src, base), + } + for _, opt := range opts { + opt(&p.opts) + } + return p +} + +func newParser(filename, src string) *_parser { + return _newParser(filename, src, 1) +} + +func ReadSource(filename string, src interface{}) ([]byte, error) { + if src != nil { + switch src := src.(type) { + case string: + return []byte(src), nil + case []byte: + return src, nil + case *bytes.Buffer: + if src != nil { + return src.Bytes(), nil + } + case io.Reader: + var bfr bytes.Buffer + if _, err := io.Copy(&bfr, src); err != nil { + return nil, err + } + return bfr.Bytes(), nil + } + return nil, errors.New("invalid source") + } + return os.ReadFile(filename) +} + +// ParseFile parses the source code of a single JavaScript/ECMAScript source file and returns +// the corresponding ast.Program node. +// +// If fileSet == nil, ParseFile parses source without a FileSet. +// If fileSet != nil, ParseFile first adds filename and src to fileSet. +// +// The filename argument is optional and is used for labelling errors, etc. +// +// src may be a string, a byte slice, a bytes.Buffer, or an io.Reader, but it MUST always be in UTF-8. +// +// // Parse some JavaScript, yielding a *ast.Program and/or an ErrorList +// program, err := parser.ParseFile(nil, "", `if (abc > 1) {}`, 0) +func ParseFile(fileSet *file.FileSet, filename string, src interface{}, mode Mode, options ...Option) (*ast.Program, error) { + str, err := ReadSource(filename, src) + if err != nil { + return nil, err + } + { + str := string(str) + + base := 1 + if fileSet != nil { + base = fileSet.AddFile(filename, str) + } + + parser := _newParser(filename, str, base, options...) + parser.mode = mode + return parser.parse() + } +} + +// ParseFunction parses a given parameter list and body as a function and returns the +// corresponding ast.FunctionLiteral node. +// +// The parameter list, if any, should be a comma-separated list of identifiers. +func ParseFunction(parameterList, body string, options ...Option) (*ast.FunctionLiteral, error) { + + src := "(function(" + parameterList + ") {\n" + body + "\n})" + + parser := _newParser("", src, 1, options...) + program, err := parser.parse() + if err != nil { + return nil, err + } + + return program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.FunctionLiteral), nil +} + +func (self *_parser) slice(idx0, idx1 file.Idx) string { + from := int(idx0) - self.base + to := int(idx1) - self.base + if from >= 0 && to <= len(self.str) { + return self.str[from:to] + } + + return "" +} + +func (self *_parser) parse() (*ast.Program, error) { + self.openScope() + defer self.closeScope() + self.next() + program := self.parseProgram() + if false { + self.errors.Sort() + } + return program, self.errors.Err() +} + +func (self *_parser) next() { + self.token, self.literal, self.parsedLiteral, self.idx = self.scan() +} + +func (self *_parser) optionalSemicolon() { + if self.token == token.SEMICOLON { + self.next() + return + } + + if self.implicitSemicolon { + self.implicitSemicolon = false + return + } + + if self.token != token.EOF && self.token != token.RIGHT_BRACE { + self.expect(token.SEMICOLON) + } +} + +func (self *_parser) semicolon() { + if self.token != token.RIGHT_PARENTHESIS && self.token != token.RIGHT_BRACE { + if self.implicitSemicolon { + self.implicitSemicolon = false + return + } + + self.expect(token.SEMICOLON) + } +} + +func (self *_parser) idxOf(offset int) file.Idx { + return file.Idx(self.base + offset) +} + +func (self *_parser) expect(value token.Token) file.Idx { + idx := self.idx + if self.token != value { + self.errorUnexpectedToken(self.token) + } + self.next() + return idx +} + +func (self *_parser) position(idx file.Idx) file.Position { + return self.file.Position(int(idx) - self.base) +} diff --git a/goja/parser/parser_test.go b/goja/parser/parser_test.go new file mode 100644 index 0000000..e09a7d6 --- /dev/null +++ b/goja/parser/parser_test.go @@ -0,0 +1,1334 @@ +package parser + +import ( + "errors" + "math/big" + "regexp" + "strings" + "testing" + + "github.com/dop251/goja/ast" + "github.com/dop251/goja/file" + "github.com/dop251/goja/token" + "github.com/dop251/goja/unistring" +) + +func firstErr(err error) error { + switch err := err.(type) { + case ErrorList: + return err[0] + } + return err +} + +var matchBeforeAfterSeparator = regexp.MustCompile(`(?m)^[ \t]*---$`) + +func testParse(src string) (parser *_parser, program *ast.Program, err error) { + defer func() { + if tmp := recover(); tmp != nil { + switch tmp := tmp.(type) { + case string: + if strings.HasPrefix(tmp, "SyntaxError:") { + parser = nil + program = nil + err = errors.New(tmp) + return + } + } + panic(tmp) + } + }() + parser = newParser("", src) + program, err = parser.parse() + return +} + +func TestParseFile(t *testing.T) { + tt(t, func() { + _, err := ParseFile(nil, "", `/abc/`, 0) + is(err, nil) + + _, err = ParseFile(nil, "", `/(?!def)abc/`, IgnoreRegExpErrors) + is(err, nil) + + _, err = ParseFile(nil, "", `/(?!def)abc/; return`, IgnoreRegExpErrors) + is(err, "(anonymous): Line 1:15 Illegal return statement") + }) +} + +func TestParseFunction(t *testing.T) { + tt(t, func() { + test := func(prm, bdy string, expect interface{}) *ast.FunctionLiteral { + function, err := ParseFunction(prm, bdy) + is(firstErr(err), expect) + return function + } + + test("a, b,c,d", "", nil) + + test("a, b;,c,d", "", "(anonymous): Line 1:15 Unexpected token ;") + + test("this", "", "(anonymous): Line 1:11 Unexpected token this") + + test("a, b, c, null", "", "(anonymous): Line 1:20 Unexpected token null") + + test("a, b,c,d", "return;", nil) + + test("a, b,c,d", "break;", "(anonymous): Line 2:1 Illegal break statement") + + test("a, b,c,d", "{}", nil) + }) +} + +func TestParserErr(t *testing.T) { + tt(t, func() { + test := func(input string, expect interface{}) (*ast.Program, *_parser) { + parser := newParser("", input) + program, err := parser.parse() + is(firstErr(err), expect) + return program, parser + } + + test("", nil) + + program, parser := test(` + var abc; + break; do { + } while(true); + `, "(anonymous): Line 3:9 Illegal break statement") + { + stmt := program.Body[1].(*ast.BadStatement) + is(parser.position(stmt.From).Column, 9) + is(parser.position(stmt.To).Column, 16) + is(parser.slice(stmt.From, stmt.To), "break; ") + } + + s := string([]byte{0x22, 0x25, 0x21, 0x63, 0x28, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3d, 0x25, 0x63, 0x25, 0x9c, 0x29, 0x25, 0x21, 0x5c, 0x28, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x3d, 0x5c, 0xe2, 0x80, 0xa9, 0x29, 0x78, 0x39, 0x63, 0x22}) + test(s, `(anonymous): Line 1:16 Invalid UTF-8 character`) + + test("{", "(anonymous): Line 1:2 Unexpected end of input") + + test("}", "(anonymous): Line 1:1 Unexpected token }") + + test("3ea", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test("3in", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test("3in []", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test("3e", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test("3e+", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test("3e-", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test("3x", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test("3x0", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test("0x", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test("09", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test("018", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test("01.0", "(anonymous): Line 1:3 Unexpected number") + + test(".0.9", "(anonymous): Line 1:3 Unexpected number") + + test("0o3e1", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test("01a", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test("0x3in[]", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test("\"Hello\nWorld\"", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test("\u203f = 10", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test("x\\", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test("x\\\\", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test("x\\u005c", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test("x\\u002a", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test("x\\\\u002a", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test("/\n", "(anonymous): Line 1:1 Invalid regular expression: missing /") + + test("0 = 1", "(anonymous): Line 1:1 Invalid left-hand side in assignment") + + test("func() = 1", "(anonymous): Line 1:1 Invalid left-hand side in assignment") + + test("(1 + 1) = 2", "(anonymous): Line 1:2 Invalid left-hand side in assignment") + + test("1++", "(anonymous): Line 1:2 Invalid left-hand side in assignment") + + test("1--", "(anonymous): Line 1:2 Invalid left-hand side in assignment") + + test("--1", "(anonymous): Line 1:1 Invalid left-hand side in assignment") + + test("for((1 + 1) in abc) def();", "(anonymous): Line 1:1 Invalid left-hand side in for-in or for-of") + + test("[", "(anonymous): Line 1:2 Unexpected end of input") + + test("[,", "(anonymous): Line 1:3 Unexpected end of input") + + test("1 + {", "(anonymous): Line 1:6 Unexpected end of input") + + test("1 + { abc:abc", "(anonymous): Line 1:14 Unexpected end of input") + + test("1 + { abc:abc,", "(anonymous): Line 1:15 Unexpected end of input") + + test("var abc = /\n/", "(anonymous): Line 1:11 Invalid regular expression: missing /") + + test("var abc = \"\n", "(anonymous): Line 1:11 Unexpected token ILLEGAL") + + test("var if = 0", "(anonymous): Line 1:5 Unexpected token if") + + test("abc + 0 = 1", "(anonymous): Line 1:1 Invalid left-hand side in assignment") + + test("+abc = 1", "(anonymous): Line 1:1 Invalid left-hand side in assignment") + + test("1 + (", "(anonymous): Line 1:6 Unexpected end of input") + + test("\n\n\n{", "(anonymous): Line 4:2 Unexpected end of input") + + test("\n/* Some multiline\ncomment */\n)", "(anonymous): Line 4:1 Unexpected token )") + + test("+1 ** 2", "(anonymous): Line 1:4 Unary operator used immediately before exponentiation expression. Parenthesis must be used to disambiguate operator precedence") + test("typeof 1 ** 2", "(anonymous): Line 1:10 Unary operator used immediately before exponentiation expression. Parenthesis must be used to disambiguate operator precedence") + + // TODO + //{ set 1 } + //{ get 2 } + //({ set: s(if) { } }) + //({ set s(.) { } }) + //({ set: s() { } }) + //({ set: s(a, b) { } }) + //({ get: g(d) { } }) + //({ get i() { }, i: 42 }) + //({ i: 42, get i() { } }) + //({ set i(x) { }, i: 42 }) + //({ i: 42, set i(x) { } }) + //({ get i() { }, get i() { } }) + //({ set i(x) { }, set i(x) { } }) + + test("function abc(if) {}", "(anonymous): Line 1:14 Unexpected token if") + + test("function abc(true) {}", "(anonymous): Line 1:14 Unexpected token true") + + test("function abc(false) {}", "(anonymous): Line 1:14 Unexpected token false") + + test("function abc(null) {}", "(anonymous): Line 1:14 Unexpected token null") + + test("function null() {}", "(anonymous): Line 1:10 Unexpected token null") + + test("function true() {}", "(anonymous): Line 1:10 Unexpected token true") + + test("function false() {}", "(anonymous): Line 1:10 Unexpected token false") + + test("function if() {}", "(anonymous): Line 1:10 Unexpected token if") + + test("a b;", "(anonymous): Line 1:3 Unexpected identifier") + + test("if.a", "(anonymous): Line 1:3 Unexpected token .") + + test("a if", "(anonymous): Line 1:3 Unexpected token if") + + test("a class", "(anonymous): Line 1:3 Unexpected token class") + + test("break\n", "(anonymous): Line 1:1 Illegal break statement") + + test("break 1;", "(anonymous): Line 1:7 Unexpected number") + + test("for (;;) { break 1; }", "(anonymous): Line 1:18 Unexpected number") + + test("continue\n", "(anonymous): Line 1:1 Illegal continue statement") + + test("continue 1;", "(anonymous): Line 1:10 Unexpected number") + + test("for (;;) { continue 1; }", "(anonymous): Line 1:21 Unexpected number") + + test("throw", "(anonymous): Line 1:1 Unexpected end of input") + + test("throw;", "(anonymous): Line 1:6 Unexpected token ;") + + test("throw \n", "(anonymous): Line 1:1 Unexpected end of input") + + test("for (var abc, def in {});", "(anonymous): Line 1:19 Unexpected token in") + + test("for ((abc in {});;);", nil) + + test("for ((abc in {}));", "(anonymous): Line 1:17 Unexpected token )") + + test("for (+abc in {});", "(anonymous): Line 1:1 Invalid left-hand side in for-in or for-of") + + test("if (false)", "(anonymous): Line 1:11 Unexpected end of input") + + test("if (false) abc(); else", "(anonymous): Line 1:23 Unexpected end of input") + + test("do", "(anonymous): Line 1:3 Unexpected end of input") + + test("while (false)", "(anonymous): Line 1:14 Unexpected end of input") + + test("for (;;)", "(anonymous): Line 1:9 Unexpected end of input") + + test("with (abc)", "(anonymous): Line 1:11 Unexpected end of input") + + test("try {}", "(anonymous): Line 1:1 Missing catch or finally after try") + + test("try {} catch () {}", "(anonymous): Line 1:15 Unexpected token )") + + test("\u203f = 1", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + // TODO + // const x = 12, y; + // const x, y = 12; + // const x; + // if(true) let a = 1; + // if(true) const a = 1; + + test(`new abc()."def"`, "(anonymous): Line 1:11 Unexpected string") + + test("/*", "(anonymous): Line 1:3 Unexpected end of input") + + test("/**", "(anonymous): Line 1:4 Unexpected end of input") + + test("/*\n\n\n", "(anonymous): Line 4:1 Unexpected end of input") + + test("/*\n\n\n*", "(anonymous): Line 4:2 Unexpected end of input") + + test("/*abc", "(anonymous): Line 1:6 Unexpected end of input") + + test("/*abc *", "(anonymous): Line 1:9 Unexpected end of input") + + test("\n]", "(anonymous): Line 2:1 Unexpected token ]") + + test("\r\n]", "(anonymous): Line 2:1 Unexpected token ]") + + test("\n\r]", "(anonymous): Line 3:1 Unexpected token ]") + + test("//\r\n]", "(anonymous): Line 2:1 Unexpected token ]") + + test("//\n\r]", "(anonymous): Line 3:1 Unexpected token ]") + + test("/abc\\\n/", "(anonymous): Line 1:1 Invalid regular expression: missing /") + + test("//\r \n]", "(anonymous): Line 3:1 Unexpected token ]") + + test("/*\r\n*/]", "(anonymous): Line 2:3 Unexpected token ]") + + test("/*\r \n*/]", "(anonymous): Line 3:3 Unexpected token ]") + + test("\\\\", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test("\\u005c", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test("\\abc", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test("\\u0000", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test("\\u200c = []", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test("\\u200D = []", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test(`"\`, "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test(`"\u`, "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test("return", "(anonymous): Line 1:1 Illegal return statement") + + test("continue", "(anonymous): Line 1:1 Illegal continue statement") + + test("break", "(anonymous): Line 1:1 Illegal break statement") + + test("switch (abc) { default: continue; }", "(anonymous): Line 1:25 Illegal continue statement") + + test("do { abc } *", "(anonymous): Line 1:12 Unexpected token *") + + test("while (true) { break abc; }", "(anonymous): Line 1:16 Undefined label 'abc'") + + test("while (true) { continue abc; }", "(anonymous): Line 1:16 Undefined label 'abc'") + + test("abc: while (true) { (function(){ break abc; }); }", "(anonymous): Line 1:34 Undefined label 'abc'") + + test("abc: while (true) { (function(){ abc: break abc; }); }", nil) + + test("abc: while (true) { (function(){ continue abc; }); }", "(anonymous): Line 1:34 Undefined label 'abc'") + + test(`abc: if (0) break abc; else {}`, nil) + + test(`abc: if (0) { break abc; } else {}`, nil) + + test(`abc: if (0) { break abc } else {}`, nil) + + test("abc: while (true) { abc: while (true) {} }", "(anonymous): Line 1:21 Label 'abc' already exists") + + test(`if(0) { do { } while(0) } else { do { } while(0) }`, nil) + + test(`if(0) do { } while(0); else do { } while(0)`, nil) + + test("_: _: while (true) {]", "(anonymous): Line 1:4 Label '_' already exists") + + test("_:\n_:\nwhile (true) {]", "(anonymous): Line 2:1 Label '_' already exists") + + test("_:\n _:\nwhile (true) {]", "(anonymous): Line 2:4 Label '_' already exists") + + test("function(){}", "(anonymous): Line 1:9 Unexpected token (") + + test("\n/*/", "(anonymous): Line 2:4 Unexpected end of input") + + test("/*/.source", "(anonymous): Line 1:11 Unexpected end of input") + + test("var class", "(anonymous): Line 1:5 Unexpected token class") + + test("var if", "(anonymous): Line 1:5 Unexpected token if") + + test("object Object", "(anonymous): Line 1:8 Unexpected identifier") + + test("[object Object]", "(anonymous): Line 1:9 Unexpected identifier") + + test("\\u0xyz", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test(`for (var abc, def in {}) {}`, "(anonymous): Line 1:19 Unexpected token in") + + test(`for (abc, def in {}) {}`, "(anonymous): Line 1:1 Invalid left-hand side in for-in or for-of") + + test(`for (var abc=def, ghi=("abc" in {}); true;) {}`, nil) + + { + // Semicolon insertion + + test("this\nif (1);", nil) + + test("while (1) { break\nif (1); }", nil) + + test("throw\nif (1);", "(anonymous): Line 1:1 Illegal newline after throw") + + test("(function(){ return\nif (1); })", nil) + + test("while (1) { continue\nif (1); }", nil) + + test("debugger\nif (1);", nil) + } + + { // Reserved words + + test("class", "(anonymous): Line 1:6 Unexpected end of input") + test("abc.class = 1", nil) + test("var class;", "(anonymous): Line 1:5 Unexpected token class") + + test("const", "(anonymous): Line 1:6 Unexpected end of input") + test("abc.const = 1", nil) + test("var const;", "(anonymous): Line 1:5 Unexpected token const") + + test("enum", "(anonymous): Line 1:1 Unexpected reserved word") + test("abc.enum = 1", nil) + test("var enum;", "(anonymous): Line 1:5 Unexpected reserved word") + + test("export", "(anonymous): Line 1:1 Unexpected reserved word") + test("abc.export = 1", nil) + test("var export;", "(anonymous): Line 1:5 Unexpected reserved word") + + test("extends", "(anonymous): Line 1:1 Unexpected token extends") + test("abc.extends = 1", nil) + test("var extends;", "(anonymous): Line 1:5 Unexpected token extends") + + test("import", "(anonymous): Line 1:1 Unexpected reserved word") + test("abc.import = 1", nil) + test("var import;", "(anonymous): Line 1:5 Unexpected reserved word") + + test("super", "(anonymous): Line 1:1 'super' keyword unexpected here") + test("abc.super = 1", nil) + test("var super;", "(anonymous): Line 1:5 Unexpected token super") + test(` + obj = { + aaa: 1 + bbb: "string" + };`, "(anonymous): Line 4:6 Unexpected identifier") + test("{}", nil) + test("{a: 1}", nil) + test("{a: 1,}", "(anonymous): Line 1:7 Unexpected token }") + test("{a: 1, b: 2}", "(anonymous): Line 1:9 Unexpected token :") + test("{a: 1, b: 2,}", "(anonymous): Line 1:9 Unexpected token :") + test(`let f = () => new import('');`, "(anonymous): Line 1:19 Unexpected reserved word") + + } + + { // Reserved words (strict) + + test(`implements`, nil) + test(`abc.implements = 1`, nil) + test(`var implements;`, nil) + + test(`interface`, nil) + test(`abc.interface = 1`, nil) + test(`var interface;`, nil) + + test(`let`, nil) + test(`abc.let = 1`, nil) + test(`var let;`, nil) + + test(`package`, nil) + test(`abc.package = 1`, nil) + test(`var package;`, nil) + + test(`private`, nil) + test(`abc.private = 1`, nil) + test(`var private;`, nil) + + test(`protected`, nil) + test(`abc.protected = 1`, nil) + test(`var protected;`, nil) + + test(`public`, nil) + test(`abc.public = 1`, nil) + test(`var public;`, nil) + + test(`static`, nil) + test(`abc.static = 1`, nil) + test(`var static;`, nil) + + test(`yield`, nil) + test(`abc.yield = 1`, nil) + test(`var yield;`, nil) + } + test(`0, { get a(param = null) {} };`, "(anonymous): Line 1:11 Getter must not have any formal parameters.") + test(`let{f(`, "(anonymous): Line 1:7 Unexpected end of input") + test("`", "(anonymous): Line 1:2 Unexpected end of input") + test(" `", "(anonymous): Line 1:3 Unexpected end of input") + test("` ", "(anonymous): Line 1:3 Unexpected end of input") + test(`var{..(`, "(anonymous): Line 1:7 Unexpected token ILLEGAL") + test(`var{get..(`, "(anonymous): Line 1:10 Unexpected token ILLEGAL") + test(`var{set..(`, "(anonymous): Line 1:10 Unexpected token ILLEGAL") + test(`(0 ?? 0 || true)`, "(anonymous): Line 1:9 Logical expressions and coalesce expressions cannot be mixed. Wrap either by parentheses") + test(`(a || b ?? c)`, "(anonymous): Line 1:9 Logical expressions and coalesce expressions cannot be mixed. Wrap either by parentheses") + test(`2 ?? 2 && 3 + 3`, "(anonymous): Line 1:3 Logical expressions and coalesce expressions cannot be mixed. Wrap either by parentheses") + test(` + class C { + st\u0061tic m() {} + } + `, "(anonymous): Line 3:25 Unexpected identifier") + }) +} + +func TestParser(t *testing.T) { + tt(t, func() { + test := func(source string, chk interface{}) *ast.Program { + _, program, err := testParse(source) + is(firstErr(err), chk) + return program + } + test(`new (() => {});`, nil) + + test(` + abc + -- + [] + `, "(anonymous): Line 3:13 Invalid left-hand side in assignment") + + test(` + abc-- + [] + `, nil) + + test("1\n[]\n", "(anonymous): Line 2:2 Unexpected token ]") + + test(` + function abc() { + } + abc() + `, nil) + + test("", nil) + + test("//", nil) + + test("/* */", nil) + + test("/** **/", nil) + + test("/*****/", nil) + + test("/*", "(anonymous): Line 1:3 Unexpected end of input") + + test("#", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test("/**/#", "(anonymous): Line 1:5 Unexpected token ILLEGAL") + + test("new +", "(anonymous): Line 1:5 Unexpected token +") + + program := test(";", nil) + is(len(program.Body), 1) + is(program.Body[0].(*ast.EmptyStatement).Semicolon, file.Idx(1)) + + program = test(";;", nil) + is(len(program.Body), 2) + is(program.Body[0].(*ast.EmptyStatement).Semicolon, file.Idx(1)) + is(program.Body[1].(*ast.EmptyStatement).Semicolon, file.Idx(2)) + + program = test("1.2", nil) + is(len(program.Body), 1) + is(program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.NumberLiteral).Literal, "1.2") + + program = test("/* */1.2", nil) + is(len(program.Body), 1) + is(program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.NumberLiteral).Literal, "1.2") + + program = test("\n", nil) + is(len(program.Body), 0) + + test(` + if (0) { + abc = 0 + } + else abc = 0 + `, nil) + + test("if (0) abc = 0 else abc = 0", "(anonymous): Line 1:16 Unexpected token else") + + test(` + if (0) { + abc = 0 + } else abc = 0 + `, nil) + + test(` + if (0) { + abc = 1 + } else { + } + `, nil) + + test(` + do { + } while (true) + `, nil) + + test(` + try { + } finally { + } + `, nil) + + test(` + try { + } catch (abc) { + } finally { + } + `, nil) + + test(` + try { + } + catch (abc) { + } + finally { + } + `, nil) + + test(`try {} catch (abc) {} finally {}`, nil) + + test("try {} catch {}", nil) + + test(` + do { + do { + } while (0) + } while (0) + `, nil) + + test(` + (function(){ + try { + if ( + 1 + ) { + return 1 + } + return 0 + } finally { + } + })() + `, nil) + + test("abc = ''\ndef", nil) + + test("abc = 1\ndef", nil) + + test("abc = Math\ndef", nil) + + test(`"\'"`, nil) + + test(` + abc = function(){ + } + abc = 0 + `, nil) + + test("abc.null = 0", nil) + + test("0x41", nil) + + test(`"\d"`, nil) + + test(`(function(){return this})`, nil) + + test(` + Object.defineProperty(Array.prototype, "0", { + value: 100, + writable: false, + configurable: true + }); + abc = [101]; + abc.hasOwnProperty("0") && abc[0] === 101; + `, nil) + + test(`new abc()`, nil) + test(`new {}`, nil) + + test(` + limit = 4 + result = 0 + while (limit) { + limit = limit - 1 + if (limit) { + } + else { + break + } + result = result + 1 + } + `, nil) + + test(` + while (0) { + if (0) { + continue + } + } + `, nil) + + test("var \u0061\u0062\u0063 = 0", nil) + + // 7_3_1 + test("var test7_3_1\nabc = 66;", nil) + test("var test7_3_1\u2028abc = 66;", nil) + + // 7_3_3 + test("//\u2028 =;", "(anonymous): Line 2:2 Unexpected token =") + + // 7_3_10 + test("var abc = \u2029;", "(anonymous): Line 2:1 Unexpected token ;") + test("var abc = \\u2029;", "(anonymous): Line 1:11 Unexpected token ILLEGAL") + test("var \\u0061\\u0062\\u0063 = 0;", nil) + + test("'", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + test("'\nstr\ning\n'", "(anonymous): Line 1:1 Unexpected token ILLEGAL") + + // S7.6_A4.3_T1 + test(`var $\u0030 = 0;`, nil) + + // S7.6.1.1_A1.1 + test(`switch = 1`, "(anonymous): Line 1:8 Unexpected token =") + + // S7.8.3_A2.1_T1 + test(`.0 === 0.0`, nil) + + // 7.8.5-1 + test("var regExp = /\\\rn/;", "(anonymous): Line 1:14 Invalid regular expression: missing /") + + // S7.8.5_A1.1_T2 + test("var regExp = /=/;", nil) + + // S7.8.5_A1.2_T1 + test("/*/", "(anonymous): Line 1:4 Unexpected end of input") + + // Sbp_7.9_A9_T3 + test(` + do { + ; + } while (false) true + `, nil) + + // S7.9_A10_T10 + test(` + {a:1 + } 3 + `, nil) + + test(` + abc + ++def + `, nil) + + // S7.9_A5.2_T1 + test(` + for(false;false + ) { + break; + } + `, "(anonymous): Line 3:13 Unexpected token )") + + // S7.9_A9_T8 + test(` + do {}; + while (false) + `, "(anonymous): Line 2:18 Unexpected token ;") + + // S8.4_A5 + test(` + "x\0y" + `, nil) + + // S9.3.1_A6_T1 + test(` + 10e10000 + `, nil) + + // 10.4.2-1-5 + test(` + "abc\ + def" + `, nil) + + test("'\\\n'", nil) + + test("'\\\r\n'", nil) + + //// 11.13.1-1-1 + test("42 = 42;", "(anonymous): Line 1:1 Invalid left-hand side in assignment") + test("s &^= 42;", "(anonymous): Line 1:4 Unexpected token ^=") + + // S11.13.2_A4.2_T1.3 + test(` + abc /= "1" + `, nil) + + // 12.1-1 + test(` + try{};catch(){} + `, "(anonymous): Line 2:13 Missing catch or finally after try") + + // 12.1-3 + test(` + try{};finally{} + `, "(anonymous): Line 2:13 Missing catch or finally after try") + + // S12.6.3_A11.1_T3 + test(` + while (true) { + break abc; + } + `, "(anonymous): Line 3:17 Undefined label 'abc'") + + // S15.3_A2_T1 + test(`var x / = 1;`, "(anonymous): Line 1:7 Unexpected token /") + + test(` + function abc() { + if (0) + return; + else { + } + } + `, nil) + + test("//\u2028 var =;", "(anonymous): Line 2:6 Unexpected token =") + + test(` + throw + {} + `, "(anonymous): Line 2:13 Illegal newline after throw") + + // S7.6.1.1_A1.11 + test(` + function = 1 + `, "(anonymous): Line 2:22 Unexpected token =") + + // S7.8.3_A1.2_T1 + test(`0e1`, nil) + + test("abc = 1; abc\n++", "(anonymous): Line 2:3 Unexpected end of input") + + // --- + + test("({ get abc() {} })", nil) + + test(`for (abc.def in {}) {}`, nil) + + test(`while (true) { break }`, nil) + + test(`while (true) { continue }`, nil) + + test(`abc=/^(?:(\w+:)\/{2}(\w+(?:\.\w+)*\/?)|(.{0,2}\/{1}))?([/.]*?(?:[^?]+)?\/)?((?:[^/?]+)\.(\w+))(?:\?(\S+)?)?$/,def=/^(?:(\w+:)\/{2})|(.{0,2}\/{1})?([/.]*?(?:[^?]+)?\/?)?$/`, nil) + + test(`(function() { try {} catch (err) {} finally {} return })`, nil) + + test(`0xde0b6b3a7640080.toFixed(0)`, nil) + + test(`/[^-._0-9A-Za-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u37f-\u1fff\u200c-\u200d\u203f\u2040\u2070-\u218f]/`, nil) + + test(`/[\u0000-\u0008\u000B-\u000C\u000E-\u001F\uD800-\uDFFF\uFFFE-\uFFFF]/`, nil) + + test("var abc = 1;\ufeff", nil) + + test("\ufeff/* var abc = 1; */", nil) + + test(`if (-0x8000000000000000<=abc&&abc<=0x8000000000000000) {}`, nil) + + test(`(function(){debugger;return this;})`, nil) + + test(` + + `, nil) + + test(` + var abc = "" + debugger + `, nil) + + test(` + var abc = /\[\]$/ + debugger + `, nil) + + test(` + var abc = 1 / + 2 + debugger + `, nil) + + test("'ё\\\u2029'", nil) + + test(`[a, b] = [1, 2]`, nil) + test(`({"a b": {}} = {})`, nil) + + test(`ref = (a, b = 39,) => { + };`, nil) + test(`(a,) => {}`, nil) + + test(`2 ?? (2 && 3) + 3`, nil) + test(`(2 ?? 2) && 3 + 3`, nil) + program = test(`a ?? b ?? c`, nil) + is(len(program.Body), 1) + is(program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.BinaryExpression).Right.(*ast.Identifier).Name, "c") + + program = test(` + class C { + a + b + #c + m() { + return this.#c; + } + } + `, nil) + is(len(program.Body), 1) + + { + program := test(`(-2)**53`, nil) + st := program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.BinaryExpression) + is(st.Operator, token.EXPONENT) + left := st.Left.(*ast.UnaryExpression) + is(left.Operator, token.MINUS) + op1 := left.Operand.(*ast.NumberLiteral) + is(op1.Literal, "2") + + right := st.Right.(*ast.NumberLiteral) + is(right.Literal, "53") + } + + }) +} + +func TestParseDestruct(t *testing.T) { + parser := newParser("", `({a: (a.b), ...spread,} = {})`) + prg, err := parser.parse() + if err != nil { + t.Fatal(err) + } + _ = prg +} + +func Test_parseStringLiteral(t *testing.T) { + tt(t, func() { + test := func(have string, want unistring.String) { + parser := newParser("", have) + parser.read() + parser.read() + _, res, err := parser.scanString(0, true) + is(err, "") + is(res, want) + } + + test(`""`, "") + test(`/=/`, "=") + + test("'1(\\\\d+)'", "1(\\d+)") + + test("'\\u2029'", "\u2029") + + test("'abc\\uFFFFabc'", "abc\uFFFFabc") + + test("'[First line \\\nSecond line \\\n Third line\\\n. ]'", + "[First line Second line Third line. ]") + + test("'\\u007a\\x79\\u000a\\x78'", "zy\nx") + + // S7.8.4_A4.2_T3 + test("'\\a'", "a") + test("'\u0410'", "\u0410") + + // S7.8.4_A5.1_T1 + test("'\\0'", "\u0000") + + // S8.4_A5 + test("'\u0000'", "\u0000") + + // 15.5.4.20 + test("\"'abc'\\\n'def'\"", "'abc''def'") + + // 15.5.4.20-4-1 + test("\"'abc'\\\r\n'def'\"", "'abc''def'") + + // Octal + test("'\\0'", "\000") + test("'\\00'", "\000") + test("'\\000'", "\000") + test("'\\09'", "\0009") + test("'\\009'", "\0009") + test("'\\0009'", "\0009") + test("'\\1'", "\001") + test("'\\01'", "\001") + test("'\\001'", "\001") + test("'\\0011'", "\0011") + test("'\\1abc'", "\001abc") + + test("'\\\u4e16'", "\u4e16") + + // err + test = func(have string, want unistring.String) { + parser := newParser("", have) + parser.read() + parser.read() + _, res, err := parser.scanString(0, true) + is(err, want) + is(res, "") + } + + test(`"\u"`, `invalid escape: \u: len("") != 4`) + test(`"\u0"`, `invalid escape: \u: len("0") != 4`) + test(`"\u00"`, `invalid escape: \u: len("00") != 4`) + test(`"\u000"`, `invalid escape: \u: len("000") != 4`) + + test(`"\x"`, `invalid escape: \x: len("") != 2`) + test(`"\x0"`, `invalid escape: \x: len("0") != 2`) + }) +} + +func Test_parseNumberLiteral(t *testing.T) { + tt(t, func() { + test := func(input string, expect interface{}) { + result, err := parseNumberLiteral(input) + is(err, nil) + is(result, expect) + } + + test("0", 0) + + test("0x8000000000000000", float64(9.223372036854776e+18)) + + test("1n", big.NewInt(1)) + + test("-1n", big.NewInt(-1)) + + test("0x23n", big.NewInt(35)) + + test("0xabcdef01n", big.NewInt(2882400001)) + + var n big.Int + n.SetString("0xabcdef0123456789abcdef0123", 0) + test("0xabcdef0123456789abcdef0123n", &n) + }) +} + +func TestPosition(t *testing.T) { + tt(t, func() { + parser := newParser("", "// Lorem ipsum") + + // Out of range, idx0 (error condition) + is(parser.slice(0, 1), "") + is(parser.slice(0, 10), "") + + // Out of range, idx1 (error condition) + is(parser.slice(1, 128), "") + + is(parser.str[0:0], "") + is(parser.slice(1, 1), "") + + is(parser.str[0:1], "/") + is(parser.slice(1, 2), "/") + + is(parser.str[0:14], "// Lorem ipsum") + is(parser.slice(1, 15), "// Lorem ipsum") + + parser = newParser("", "(function(){ return 0; })") + program, err := parser.parse() + is(err, nil) + + var node ast.Node + node = program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.FunctionLiteral) + is(node.Idx0(), file.Idx(2)) + is(node.Idx1(), file.Idx(25)) + is(parser.slice(node.Idx0(), node.Idx1()), "function(){ return 0; }") + is(parser.slice(node.Idx0(), node.Idx1()+1), "function(){ return 0; })") + is(parser.slice(node.Idx0(), node.Idx1()+2), "") + is(node.(*ast.FunctionLiteral).Source, "function(){ return 0; }") + + node = program + is(node.Idx0(), file.Idx(2)) + is(node.Idx1(), file.Idx(25)) + is(parser.slice(node.Idx0(), node.Idx1()), "function(){ return 0; }") + + parser = newParser("", "(function(){ return abc; })") + program, err = parser.parse() + is(err, nil) + node = program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.FunctionLiteral) + is(node.(*ast.FunctionLiteral).Source, "function(){ return abc; }") + node = program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.FunctionLiteral).Body.List[0].(*ast.ReturnStatement) + is(parser.slice(node.Idx0(), node.Idx1()), "return abc") + + parser = newParser("", "(function(){ return; })") + program, err = parser.parse() + is(err, nil) + node = program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.FunctionLiteral).Body.List[0].(*ast.ReturnStatement) + is(parser.slice(node.Idx0(), node.Idx1()), "return") + + parser = newParser("", "true ? 1 : 2") + program, err = parser.parse() + is(err, nil) + node = program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.ConditionalExpression) + is(parser.slice(node.Idx0(), node.Idx1()), "true ? 1 : 2") + + parser = newParser("", "a++") + program, err = parser.parse() + is(err, nil) + node = program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.UnaryExpression) + is(parser.slice(node.Idx0(), node.Idx1()), "a++") + + parser = newParser("", "++a") + program, err = parser.parse() + is(err, nil) + node = program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.UnaryExpression) + is(parser.slice(node.Idx0(), node.Idx1()), "++a") + + parser = newParser("", "xyz: for (i = 0; i < 10; i++) { if (i == 5) continue xyz; }") + program, err = parser.parse() + is(err, nil) + node = program.Body[0].(*ast.LabelledStatement) + is(parser.slice(node.Idx0(), node.Idx1()), "xyz: for (i = 0; i < 10; i++) { if (i == 5) continue xyz; }") + node = program.Body[0].(*ast.LabelledStatement).Statement.(*ast.ForStatement) + is(parser.slice(node.Idx0(), node.Idx1()), "for (i = 0; i < 10; i++) { if (i == 5) continue xyz; }") + block := program.Body[0].(*ast.LabelledStatement).Statement.(*ast.ForStatement).Body.(*ast.BlockStatement) + node = block.List[0].(*ast.IfStatement).Consequent.(*ast.BranchStatement) + is(parser.slice(node.Idx0(), node.Idx1()), "continue xyz") + + parser = newParser("", "for (p in o) { break; }") + program, err = parser.parse() + is(err, nil) + node = program.Body[0].(*ast.ForInStatement) + is(parser.slice(node.Idx0(), node.Idx1()), "for (p in o) { break; }") + node = program.Body[0].(*ast.ForInStatement).Body.(*ast.BlockStatement).List[0].(*ast.BranchStatement) + is(parser.slice(node.Idx0(), node.Idx1()), "break") + + parser = newParser("", "while (i < 10) { i++; }") + program, err = parser.parse() + is(err, nil) + node = program.Body[0].(*ast.WhileStatement) + is(parser.slice(node.Idx0(), node.Idx1()), "while (i < 10) { i++; }") + + parser = newParser("", "do { i++; } while (i < 10 )") + program, err = parser.parse() + is(err, nil) + node = program.Body[0].(*ast.DoWhileStatement) + is(parser.slice(node.Idx0(), node.Idx1()), "do { i++; } while (i < 10 )") + + parser = newParser("", "with (1) {}") + program, err = parser.parse() + is(err, nil) + node = program.Body[0].(*ast.WithStatement) + is(parser.slice(node.Idx0(), node.Idx1()), "with (1) {}") + + parser = newParser("", `switch (a) { + case 1: x--; + case 2: + default: x++; +}`) + program, err = parser.parse() + is(err, nil) + node = program.Body[0].(*ast.SwitchStatement) + is(parser.slice(node.Idx0(), node.Idx1()), `switch (a) { + case 1: x--; + case 2: + default: x++; +}`) + }) +} + +func TestExtractSourceMapLine(t *testing.T) { + tt(t, func() { + is(extractSourceMapLine(""), "") + is(extractSourceMapLine("\n"), "") + is(extractSourceMapLine(" "), "") + is(extractSourceMapLine("1\n2\n3\n4\n"), "") + + src := `"use strict"; +var x = {}; +//# sourceMappingURL=delme.js.map` + modSrc := `(function(exports, require, module) {` + src + ` +})` + is(extractSourceMapLine(modSrc), "//# sourceMappingURL=delme.js.map") + is(extractSourceMapLine(modSrc+"\n\n\n\n"), "//# sourceMappingURL=delme.js.map") + }) +} + +func TestSourceMapOptions(t *testing.T) { + tt(t, func() { + count := 0 + requestedPath := "" + loader := func(p string) ([]byte, error) { + count++ + requestedPath = p + return nil, nil + } + src := `"use strict"; +var x = {}; +//# sourceMappingURL=delme.js.map` + _, err := ParseFile(nil, "delme.js", src, 0, WithSourceMapLoader(loader)) + is(err, nil) + is(count, 1) + is(requestedPath, "delme.js.map") + + count = 0 + _, err = ParseFile(nil, "", src, 0, WithSourceMapLoader(loader)) + is(err, nil) + is(count, 1) + is(requestedPath, "delme.js.map") + + count = 0 + _, err = ParseFile(nil, "delme.js", src, 0, WithDisableSourceMaps) + is(err, nil) + is(count, 0) + + _, err = ParseFile(nil, "/home/user/src/delme.js", src, 0, WithSourceMapLoader(loader)) + is(err, nil) + is(count, 1) + is(requestedPath, "/home/user/src/delme.js.map") + + count = 0 + _, err = ParseFile(nil, "https://site.com/delme.js", src, 0, WithSourceMapLoader(loader)) + is(err, nil) + is(count, 1) + is(requestedPath, "https://site.com/delme.js.map") + }) +} + +func TestParseTemplateCharacters(t *testing.T) { + parser := newParser("", "`test\\\r\\\n${a}`") + parser.next() + if parser.token != token.BACKTICK { + t.Fatalf("Token: %s", parser.token) + } + checkParseTemplateChars := func(expectedLiteral string, expectedParsed unistring.String, expectedFinished, expectParseErr, expectErr bool) { + literal, parsed, finished, parseErr, err := parser.parseTemplateCharacters() + if err != "" != expectErr { + t.Fatal(err) + } + if literal != expectedLiteral { + t.Fatalf("Literal: %q", literal) + } + if parsed != expectedParsed { + t.Fatalf("Parsed: %q", parsed) + } + if finished != expectedFinished { + t.Fatal(finished) + } + if parseErr != "" != expectParseErr { + t.Fatalf("parseErr: %v", parseErr) + } + } + checkParseTemplateChars("test\\\n\\\n", "test", false, false, false) + parser.next() + parser.expect(token.IDENTIFIER) + if len(parser.errors) > 0 { + t.Fatal(parser.errors) + } + if parser.token != token.RIGHT_BRACE { + t.Fatal("Expected }") + } + if len(parser.errors) > 0 { + t.Fatal(parser.errors) + } + checkParseTemplateChars("", "", true, false, false) + if parser.chr != -1 { + t.Fatal("Expected EOF") + } +} + +func TestParseTemplateLiteral(t *testing.T) { + parser := newParser("", "f()\n`test${a}`") + prg, err := parser.parse() + if err != nil { + t.Fatal(err) + } + if st, ok := prg.Body[0].(*ast.ExpressionStatement); ok { + if expr, ok := st.Expression.(*ast.TemplateLiteral); ok { + if expr.Tag == nil { + t.Fatal("tag is nil") + } + if idx0 := expr.Tag.Idx0(); idx0 != 1 { + t.Fatalf("Tag.Idx0(): %d", idx0) + } + if expr.OpenQuote != 5 { + t.Fatalf("OpenQuote: %d", expr.OpenQuote) + } + if expr.CloseQuote != 14 { + t.Fatalf("CloseQuote: %d", expr.CloseQuote) + } + if l := len(expr.Elements); l != 2 { + t.Fatalf("len elements: %d", l) + } + if l := len(expr.Expressions); l != 1 { + t.Fatalf("len expressions: %d", l) + } + } else { + t.Fatal(st) + } + } else { + t.Fatal(prg.Body[0]) + } +} + +func TestParseTemplateLiteralWithTail(t *testing.T) { + parser := newParser("", "f()\n`test${a}tail` ") + prg, err := parser.parse() + if err != nil { + t.Fatal(err) + } + if st, ok := prg.Body[0].(*ast.ExpressionStatement); ok { + if expr, ok := st.Expression.(*ast.TemplateLiteral); ok { + if expr.CloseQuote != 18 { + t.Fatalf("CloseQuote: %d", expr.CloseQuote) + } + } else { + t.Fatal(st) + } + } else { + t.Fatal(prg.Body[0]) + } +} diff --git a/goja/parser/regexp.go b/goja/parser/regexp.go new file mode 100644 index 0000000..f455d0d --- /dev/null +++ b/goja/parser/regexp.go @@ -0,0 +1,472 @@ +package parser + +import ( + "fmt" + "strconv" + "strings" + "unicode/utf8" +) + +const ( + WhitespaceChars = " \f\n\r\t\v\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000\ufeff" + Re2Dot = "[^\r\n\u2028\u2029]" +) + +type regexpParseError struct { + offset int + err string +} + +type RegexpErrorIncompatible struct { + regexpParseError +} +type RegexpSyntaxError struct { + regexpParseError +} + +func (s regexpParseError) Error() string { + return s.err +} + +type _RegExp_parser struct { + str string + length int + + chr rune // The current character + chrOffset int // The offset of current character + offset int // The offset after current character (may be greater than 1) + + err error + + goRegexp strings.Builder + passOffset int + + dotAll bool // Enable dotAll mode + unicode bool +} + +// TransformRegExp transforms a JavaScript pattern into a Go "regexp" pattern. +// +// re2 (Go) cannot do backtracking, so the presence of a lookahead (?=) (?!) or +// backreference (\1, \2, ...) will cause an error. +// +// re2 (Go) has a different definition for \s: [\t\n\f\r ]. +// The JavaScript definition, on the other hand, also includes \v, Unicode "Separator, Space", etc. +// +// If the pattern is valid, but incompatible (contains a lookahead or backreference), +// then this function returns an empty string an error of type RegexpErrorIncompatible. +// +// If the pattern is invalid (not valid even in JavaScript), then this function +// returns an empty string and a generic error. +func TransformRegExp(pattern string, dotAll, unicode bool) (transformed string, err error) { + + if pattern == "" { + return "", nil + } + + parser := _RegExp_parser{ + str: pattern, + length: len(pattern), + dotAll: dotAll, + unicode: unicode, + } + err = parser.parse() + if err != nil { + return "", err + } + + return parser.ResultString(), nil +} + +func (self *_RegExp_parser) ResultString() string { + if self.passOffset != -1 { + return self.str[:self.passOffset] + } + return self.goRegexp.String() +} + +func (self *_RegExp_parser) parse() (err error) { + self.read() // Pull in the first character + self.scan() + return self.err +} + +func (self *_RegExp_parser) read() { + if self.offset < self.length { + self.chrOffset = self.offset + chr, width := rune(self.str[self.offset]), 1 + if chr >= utf8.RuneSelf { // !ASCII + chr, width = utf8.DecodeRuneInString(self.str[self.offset:]) + if chr == utf8.RuneError && width == 1 { + self.error(true, "Invalid UTF-8 character") + return + } + } + self.offset += width + self.chr = chr + } else { + self.chrOffset = self.length + self.chr = -1 // EOF + } +} + +func (self *_RegExp_parser) stopPassing() { + self.goRegexp.Grow(3 * len(self.str) / 2) + self.goRegexp.WriteString(self.str[:self.passOffset]) + self.passOffset = -1 +} + +func (self *_RegExp_parser) write(p []byte) { + if self.passOffset != -1 { + self.stopPassing() + } + self.goRegexp.Write(p) +} + +func (self *_RegExp_parser) writeByte(b byte) { + if self.passOffset != -1 { + self.stopPassing() + } + self.goRegexp.WriteByte(b) +} + +func (self *_RegExp_parser) writeString(s string) { + if self.passOffset != -1 { + self.stopPassing() + } + self.goRegexp.WriteString(s) +} + +func (self *_RegExp_parser) scan() { + for self.chr != -1 { + switch self.chr { + case '\\': + self.read() + self.scanEscape(false) + case '(': + self.pass() + self.scanGroup() + case '[': + self.scanBracket() + case ')': + self.error(true, "Unmatched ')'") + return + case '.': + if self.dotAll { + self.pass() + break + } + self.writeString(Re2Dot) + self.read() + default: + self.pass() + } + } +} + +// (...) +func (self *_RegExp_parser) scanGroup() { + str := self.str[self.chrOffset:] + if len(str) > 1 { // A possibility of (?= or (?! + if str[0] == '?' { + ch := str[1] + switch { + case ch == '=' || ch == '!': + self.error(false, "re2: Invalid (%s) ", self.str[self.chrOffset:self.chrOffset+2]) + return + case ch == '<': + self.error(false, "re2: Invalid (%s) ", self.str[self.chrOffset:self.chrOffset+2]) + return + case ch != ':': + self.error(true, "Invalid group") + return + } + } + } + for self.chr != -1 && self.chr != ')' { + switch self.chr { + case '\\': + self.read() + self.scanEscape(false) + case '(': + self.pass() + self.scanGroup() + case '[': + self.scanBracket() + case '.': + if self.dotAll { + self.pass() + break + } + self.writeString(Re2Dot) + self.read() + default: + self.pass() + continue + } + } + if self.chr != ')' { + self.error(true, "Unterminated group") + return + } + self.pass() +} + +// [...] +func (self *_RegExp_parser) scanBracket() { + str := self.str[self.chrOffset:] + if strings.HasPrefix(str, "[]") { + // [] -- Empty character class + self.writeString("[^\u0000-\U0001FFFF]") + self.offset += 1 + self.read() + return + } + + if strings.HasPrefix(str, "[^]") { + self.writeString("[\u0000-\U0001FFFF]") + self.offset += 2 + self.read() + return + } + + self.pass() + for self.chr != -1 { + if self.chr == ']' { + break + } else if self.chr == '\\' { + self.read() + self.scanEscape(true) + continue + } + self.pass() + } + if self.chr != ']' { + self.error(true, "Unterminated character class") + return + } + self.pass() +} + +// \... +func (self *_RegExp_parser) scanEscape(inClass bool) { + offset := self.chrOffset + + var length, base uint32 + switch self.chr { + + case '0', '1', '2', '3', '4', '5', '6', '7': + var value int64 + size := 0 + for { + digit := int64(digitValue(self.chr)) + if digit >= 8 { + // Not a valid digit + break + } + value = value*8 + digit + self.read() + size += 1 + } + if size == 1 { // The number of characters read + if value != 0 { + // An invalid backreference + self.error(false, "re2: Invalid \\%d ", value) + return + } + self.passString(offset-1, self.chrOffset) + return + } + tmp := []byte{'\\', 'x', '0', 0} + if value >= 16 { + tmp = tmp[0:2] + } else { + tmp = tmp[0:3] + } + tmp = strconv.AppendInt(tmp, value, 16) + self.write(tmp) + return + + case '8', '9': + self.read() + self.error(false, "re2: Invalid \\%s ", self.str[offset:self.chrOffset]) + return + + case 'x': + self.read() + length, base = 2, 16 + + case 'u': + self.read() + if self.chr == '{' && self.unicode { + self.read() + length, base = 0, 16 + } else { + length, base = 4, 16 + } + + case 'b': + if inClass { + self.write([]byte{'\\', 'x', '0', '8'}) + self.read() + return + } + fallthrough + + case 'B': + fallthrough + + case 'd', 'D', 'w', 'W': + // This is slightly broken, because ECMAScript + // includes \v in \s, \S, while re2 does not + fallthrough + + case '\\': + fallthrough + + case 'f', 'n', 'r', 't', 'v': + self.passString(offset-1, self.offset) + self.read() + return + + case 'c': + self.read() + var value int64 + if 'a' <= self.chr && self.chr <= 'z' { + value = int64(self.chr - 'a' + 1) + } else if 'A' <= self.chr && self.chr <= 'Z' { + value = int64(self.chr - 'A' + 1) + } else { + self.writeByte('c') + return + } + tmp := []byte{'\\', 'x', '0', 0} + if value >= 16 { + tmp = tmp[0:2] + } else { + tmp = tmp[0:3] + } + tmp = strconv.AppendInt(tmp, value, 16) + self.write(tmp) + self.read() + return + case 's': + if inClass { + self.writeString(WhitespaceChars) + } else { + self.writeString("[" + WhitespaceChars + "]") + } + self.read() + return + case 'S': + if inClass { + self.error(false, "S in class") + return + } else { + self.writeString("[^" + WhitespaceChars + "]") + } + self.read() + return + default: + // $ is an identifier character, so we have to have + // a special case for it here + if self.chr == '$' || self.chr < utf8.RuneSelf && !isIdentifierPart(self.chr) { + // A non-identifier character needs escaping + self.passString(offset-1, self.offset) + self.read() + return + } + // Unescape the character for re2 + self.pass() + return + } + + // Otherwise, we're a \u.... or \x... + valueOffset := self.chrOffset + + if length > 0 { + for length := length; length > 0; length-- { + digit := uint32(digitValue(self.chr)) + if digit >= base { + // Not a valid digit + goto skip + } + self.read() + } + } else { + for self.chr != '}' && self.chr != -1 { + digit := uint32(digitValue(self.chr)) + if digit >= base { + // Not a valid digit + self.error(true, "Invalid Unicode escape") + return + } + self.read() + } + } + + if length == 4 || length == 0 { + self.write([]byte{ + '\\', + 'x', + '{', + }) + self.passString(valueOffset, self.chrOffset) + if length != 0 { + self.writeByte('}') + } + } else if length == 2 { + self.passString(offset-1, valueOffset+2) + } else { + // Should never, ever get here... + self.error(true, "re2: Illegal branch in scanEscape") + return + } + + return + +skip: + self.passString(offset, self.chrOffset) +} + +func (self *_RegExp_parser) pass() { + if self.passOffset == self.chrOffset { + self.passOffset = self.offset + } else { + if self.passOffset != -1 { + self.stopPassing() + } + if self.chr != -1 { + self.goRegexp.WriteRune(self.chr) + } + } + self.read() +} + +func (self *_RegExp_parser) passString(start, end int) { + if self.passOffset == start { + self.passOffset = end + return + } + if self.passOffset != -1 { + self.stopPassing() + } + self.goRegexp.WriteString(self.str[start:end]) +} + +func (self *_RegExp_parser) error(fatal bool, msg string, msgValues ...interface{}) { + if self.err != nil { + return + } + e := regexpParseError{ + offset: self.offset, + err: fmt.Sprintf(msg, msgValues...), + } + if fatal { + self.err = RegexpSyntaxError{e} + } else { + self.err = RegexpErrorIncompatible{e} + } + self.offset = self.length + self.chr = -1 +} diff --git a/goja/parser/regexp_test.go b/goja/parser/regexp_test.go new file mode 100644 index 0000000..3be77a3 --- /dev/null +++ b/goja/parser/regexp_test.go @@ -0,0 +1,191 @@ +package parser + +import ( + "regexp" + "testing" +) + +func TestRegExp(t *testing.T) { + tt(t, func() { + { + // err + test := func(input string, expect interface{}) { + _, err := TransformRegExp(input, false, false) + _, incompat := err.(RegexpErrorIncompatible) + is(incompat, false) + is(err, expect) + } + + test("[", "Unterminated character class") + + test("(", "Unterminated group") + + test("\\(?=)", "Unmatched ')'") + + test(")", "Unmatched ')'") + test("0:(?)", "Invalid group") + test("(?)", "Invalid group") + test("(?U)", "Invalid group") + test("(?)|(?i)", "Invalid group") + test("(?P)(?P)(?P)", "Invalid group") + } + + { + // incompatible + test := func(input string, expectErr interface{}) { + _, err := TransformRegExp(input, false, false) + _, incompat := err.(RegexpErrorIncompatible) + is(incompat, true) + is(err, expectErr) + } + + test(`<%([\s\S]+?)%>`, "S in class") + + test("(?<=y)x", "re2: Invalid (?<) ") + + test(`(?!test)`, "re2: Invalid (?!) ") + + test(`\1`, "re2: Invalid \\1 ") + + test(`\8`, "re2: Invalid \\8 ") + + } + + { + // err + test := func(input string, expect string) { + result, err := TransformRegExp(input, false, false) + is(err, nil) + _, incompat := err.(RegexpErrorIncompatible) + is(incompat, false) + is(result, expect) + _, err = regexp.Compile(result) + is(err, nil) + } + + test("", "") + + test("abc", "abc") + + test(`\abc`, `abc`) + + test(`\a\b\c`, `a\bc`) + + test(`\x`, `x`) + + test(`\c`, `c`) + + test(`\cA`, `\x01`) + + test(`\cz`, `\x1a`) + + test(`\ca`, `\x01`) + + test(`\cj`, `\x0a`) + + test(`\ck`, `\x0b`) + + test(`\+`, `\+`) + + test(`[\b]`, `[\x08]`) + + test(`\u0z01\x\undefined`, `u0z01xundefined`) + + test(`\\|'|\r|\n|\t|\u2028|\u2029`, `\\|'|\r|\n|\t|\x{2028}|\x{2029}`) + + test("]", "]") + + test("}", "}") + + test("%", "%") + + test("(%)", "(%)") + + test("(?:[%\\s])", "(?:[%"+WhitespaceChars+"])") + + test("[[]", "[[]") + + test("\\101", "\\x41") + + test("\\51", "\\x29") + + test("\\051", "\\x29") + + test("\\175", "\\x7d") + + test("\\0", "\\0") + + test("\\04", "\\x04") + + test(`(.)^`, "("+Re2Dot+")^") + + test(`\$`, `\$`) + + test(`[G-b]`, `[G-b]`) + + test(`[G-b\0]`, `[G-b\0]`) + + test(`\k`, `k`) + + test(`\x20`, `\x20`) + + test(`😊`, `😊`) + + test(`^.*`, `^`+Re2Dot+`*`) + + test(`(\n)`, `(\n)`) + + test(`(a(bc))`, `(a(bc))`) + + test(`[]`, "[^\u0000-\U0001FFFF]") + + test(`[^]`, "[\u0000-\U0001FFFF]") + + test(`\s+`, "["+WhitespaceChars+"]+") + + test(`\S+`, "[^"+WhitespaceChars+"]+") + + } + }) +} + +func TestTransformRegExp(t *testing.T) { + tt(t, func() { + pattern, err := TransformRegExp(`\s+abc\s+`, false, false) + is(err, nil) + is(pattern, `[`+WhitespaceChars+`]+abc[`+WhitespaceChars+`]+`) + is(regexp.MustCompile(pattern).MatchString("\t abc def"), true) + }) + tt(t, func() { + pattern, err := TransformRegExp(`\u{1d306}`, false, true) + is(err, nil) + is(pattern, `\x{1d306}`) + }) + tt(t, func() { + pattern, err := TransformRegExp(`\u1234`, false, false) + is(err, nil) + is(pattern, `\x{1234}`) + }) +} + +func BenchmarkTransformRegExp(b *testing.B) { + f := func(reStr string, b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _, _ = TransformRegExp(reStr, false, false) + } + } + + b.Run("Re", func(b *testing.B) { + f(`^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$`, b) + }) + + b.Run("Re2-1", func(b *testing.B) { + f(`(?=)^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$`, b) + }) + + b.Run("Re2-1", func(b *testing.B) { + f(`^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$(?=)`, b) + }) +} diff --git a/goja/parser/scope.go b/goja/parser/scope.go new file mode 100644 index 0000000..5e28ef4 --- /dev/null +++ b/goja/parser/scope.go @@ -0,0 +1,50 @@ +package parser + +import ( + "github.com/dop251/goja/ast" + "github.com/dop251/goja/unistring" +) + +type _scope struct { + outer *_scope + allowIn bool + allowLet bool + inIteration bool + inSwitch bool + inFuncParams bool + inFunction bool + inAsync bool + allowAwait bool + allowYield bool + declarationList []*ast.VariableDeclaration + + labels []unistring.String +} + +func (self *_parser) openScope() { + self.scope = &_scope{ + outer: self.scope, + allowIn: true, + } +} + +func (self *_parser) closeScope() { + self.scope = self.scope.outer +} + +func (self *_scope) declare(declaration *ast.VariableDeclaration) { + self.declarationList = append(self.declarationList, declaration) +} + +func (self *_scope) hasLabel(name unistring.String) bool { + for _, label := range self.labels { + if label == name { + return true + } + } + if self.outer != nil && !self.inFunction { + // Crossing a function boundary to look for a label is verboten + return self.outer.hasLabel(name) + } + return false +} diff --git a/goja/parser/statement.go b/goja/parser/statement.go new file mode 100644 index 0000000..8ec5cde --- /dev/null +++ b/goja/parser/statement.go @@ -0,0 +1,1078 @@ +package parser + +import ( + "encoding/base64" + "fmt" + "os" + "strings" + + "github.com/dop251/goja/ast" + "github.com/dop251/goja/file" + "github.com/dop251/goja/token" + "github.com/go-sourcemap/sourcemap" +) + +func (self *_parser) parseBlockStatement() *ast.BlockStatement { + node := &ast.BlockStatement{} + node.LeftBrace = self.expect(token.LEFT_BRACE) + node.List = self.parseStatementList() + node.RightBrace = self.expect(token.RIGHT_BRACE) + + return node +} + +func (self *_parser) parseEmptyStatement() ast.Statement { + idx := self.expect(token.SEMICOLON) + return &ast.EmptyStatement{Semicolon: idx} +} + +func (self *_parser) parseStatementList() (list []ast.Statement) { + for self.token != token.RIGHT_BRACE && self.token != token.EOF { + self.scope.allowLet = true + list = append(list, self.parseStatement()) + } + + return +} + +func (self *_parser) parseStatement() ast.Statement { + + if self.token == token.EOF { + self.errorUnexpectedToken(self.token) + return &ast.BadStatement{From: self.idx, To: self.idx + 1} + } + + switch self.token { + case token.SEMICOLON: + return self.parseEmptyStatement() + case token.LEFT_BRACE: + return self.parseBlockStatement() + case token.IF: + return self.parseIfStatement() + case token.DO: + return self.parseDoWhileStatement() + case token.WHILE: + return self.parseWhileStatement() + case token.FOR: + return self.parseForOrForInStatement() + case token.BREAK: + return self.parseBreakStatement() + case token.CONTINUE: + return self.parseContinueStatement() + case token.DEBUGGER: + return self.parseDebuggerStatement() + case token.WITH: + return self.parseWithStatement() + case token.VAR: + return self.parseVariableStatement() + case token.LET: + tok := self.peek() + if tok == token.LEFT_BRACKET || self.scope.allowLet && (token.IsId(tok) || tok == token.LEFT_BRACE) { + return self.parseLexicalDeclaration(self.token) + } + self.insertSemicolon = true + case token.CONST: + return self.parseLexicalDeclaration(self.token) + case token.ASYNC: + if f := self.parseMaybeAsyncFunction(true); f != nil { + return &ast.FunctionDeclaration{ + Function: f, + } + } + case token.FUNCTION: + return &ast.FunctionDeclaration{ + Function: self.parseFunction(true, false, self.idx), + } + case token.CLASS: + return &ast.ClassDeclaration{ + Class: self.parseClass(true), + } + case token.SWITCH: + return self.parseSwitchStatement() + case token.RETURN: + return self.parseReturnStatement() + case token.THROW: + return self.parseThrowStatement() + case token.TRY: + return self.parseTryStatement() + } + + expression := self.parseExpression() + + if identifier, isIdentifier := expression.(*ast.Identifier); isIdentifier && self.token == token.COLON { + // LabelledStatement + colon := self.idx + self.next() // : + label := identifier.Name + for _, value := range self.scope.labels { + if label == value { + self.error(identifier.Idx0(), "Label '%s' already exists", label) + } + } + self.scope.labels = append(self.scope.labels, label) // Push the label + self.scope.allowLet = false + statement := self.parseStatement() + self.scope.labels = self.scope.labels[:len(self.scope.labels)-1] // Pop the label + return &ast.LabelledStatement{ + Label: identifier, + Colon: colon, + Statement: statement, + } + } + + self.optionalSemicolon() + + return &ast.ExpressionStatement{ + Expression: expression, + } +} + +func (self *_parser) parseTryStatement() ast.Statement { + + node := &ast.TryStatement{ + Try: self.expect(token.TRY), + Body: self.parseBlockStatement(), + } + + if self.token == token.CATCH { + catch := self.idx + self.next() + var parameter ast.BindingTarget + if self.token == token.LEFT_PARENTHESIS { + self.next() + parameter = self.parseBindingTarget() + self.expect(token.RIGHT_PARENTHESIS) + } + node.Catch = &ast.CatchStatement{ + Catch: catch, + Parameter: parameter, + Body: self.parseBlockStatement(), + } + } + + if self.token == token.FINALLY { + self.next() + node.Finally = self.parseBlockStatement() + } + + if node.Catch == nil && node.Finally == nil { + self.error(node.Try, "Missing catch or finally after try") + return &ast.BadStatement{From: node.Try, To: node.Body.Idx1()} + } + + return node +} + +func (self *_parser) parseFunctionParameterList() *ast.ParameterList { + opening := self.expect(token.LEFT_PARENTHESIS) + var list []*ast.Binding + var rest ast.Expression + if !self.scope.inFuncParams { + self.scope.inFuncParams = true + defer func() { + self.scope.inFuncParams = false + }() + } + for self.token != token.RIGHT_PARENTHESIS && self.token != token.EOF { + if self.token == token.ELLIPSIS { + self.next() + rest = self.reinterpretAsDestructBindingTarget(self.parseAssignmentExpression()) + break + } + self.parseVariableDeclaration(&list) + if self.token != token.RIGHT_PARENTHESIS { + self.expect(token.COMMA) + } + } + closing := self.expect(token.RIGHT_PARENTHESIS) + + return &ast.ParameterList{ + Opening: opening, + List: list, + Rest: rest, + Closing: closing, + } +} + +func (self *_parser) parseMaybeAsyncFunction(declaration bool) *ast.FunctionLiteral { + if self.peek() == token.FUNCTION { + idx := self.idx + self.next() + return self.parseFunction(declaration, true, idx) + } + return nil +} + +func (self *_parser) parseFunction(declaration, async bool, start file.Idx) *ast.FunctionLiteral { + + node := &ast.FunctionLiteral{ + Function: start, + Async: async, + } + self.expect(token.FUNCTION) + + if self.token == token.MULTIPLY { + node.Generator = true + self.next() + } + + if !declaration { + if async != self.scope.allowAwait { + self.scope.allowAwait = async + defer func() { + self.scope.allowAwait = !async + }() + } + if node.Generator != self.scope.allowYield { + self.scope.allowYield = node.Generator + defer func() { + self.scope.allowYield = !node.Generator + }() + } + } + + self.tokenToBindingId() + var name *ast.Identifier + if self.token == token.IDENTIFIER { + name = self.parseIdentifier() + } else if declaration { + // Use expect error handling + self.expect(token.IDENTIFIER) + } + node.Name = name + + if declaration { + if async != self.scope.allowAwait { + self.scope.allowAwait = async + defer func() { + self.scope.allowAwait = !async + }() + } + if node.Generator != self.scope.allowYield { + self.scope.allowYield = node.Generator + defer func() { + self.scope.allowYield = !node.Generator + }() + } + } + + node.ParameterList = self.parseFunctionParameterList() + node.Body, node.DeclarationList = self.parseFunctionBlock(async, async, self.scope.allowYield) + node.Source = self.slice(node.Idx0(), node.Idx1()) + + return node +} + +func (self *_parser) parseFunctionBlock(async, allowAwait, allowYield bool) (body *ast.BlockStatement, declarationList []*ast.VariableDeclaration) { + self.openScope() + self.scope.inFunction = true + self.scope.inAsync = async + self.scope.allowAwait = allowAwait + self.scope.allowYield = allowYield + defer self.closeScope() + body = self.parseBlockStatement() + declarationList = self.scope.declarationList + return +} + +func (self *_parser) parseArrowFunctionBody(async bool) (ast.ConciseBody, []*ast.VariableDeclaration) { + if self.token == token.LEFT_BRACE { + return self.parseFunctionBlock(async, async, false) + } + if async != self.scope.inAsync || async != self.scope.allowAwait { + inAsync := self.scope.inAsync + allowAwait := self.scope.allowAwait + self.scope.inAsync = async + self.scope.allowAwait = async + allowYield := self.scope.allowYield + self.scope.allowYield = false + defer func() { + self.scope.inAsync = inAsync + self.scope.allowAwait = allowAwait + self.scope.allowYield = allowYield + }() + } + + return &ast.ExpressionBody{ + Expression: self.parseAssignmentExpression(), + }, nil +} + +func (self *_parser) parseClass(declaration bool) *ast.ClassLiteral { + if !self.scope.allowLet && self.token == token.CLASS { + self.errorUnexpectedToken(token.CLASS) + } + + node := &ast.ClassLiteral{ + Class: self.expect(token.CLASS), + } + + self.tokenToBindingId() + var name *ast.Identifier + if self.token == token.IDENTIFIER { + name = self.parseIdentifier() + } else if declaration { + // Use expect error handling + self.expect(token.IDENTIFIER) + } + + node.Name = name + + if self.token != token.LEFT_BRACE { + self.expect(token.EXTENDS) + node.SuperClass = self.parseLeftHandSideExpressionAllowCall() + } + + self.expect(token.LEFT_BRACE) + + for self.token != token.RIGHT_BRACE && self.token != token.EOF { + if self.token == token.SEMICOLON { + self.next() + continue + } + start := self.idx + static := false + if self.token == token.STATIC { + switch self.peek() { + case token.ASSIGN, token.SEMICOLON, token.RIGHT_BRACE, token.LEFT_PARENTHESIS: + // treat as identifier + default: + self.next() + if self.token == token.LEFT_BRACE { + b := &ast.ClassStaticBlock{ + Static: start, + } + b.Block, b.DeclarationList = self.parseFunctionBlock(false, true, false) + b.Source = self.slice(b.Block.LeftBrace, b.Block.Idx1()) + node.Body = append(node.Body, b) + continue + } + static = true + } + } + + var kind ast.PropertyKind + var async bool + methodBodyStart := self.idx + if self.literal == "get" || self.literal == "set" { + if tok := self.peek(); tok != token.SEMICOLON && tok != token.LEFT_PARENTHESIS { + if self.literal == "get" { + kind = ast.PropertyKindGet + } else { + kind = ast.PropertyKindSet + } + self.next() + } + } else if self.token == token.ASYNC { + if tok := self.peek(); tok != token.SEMICOLON && tok != token.LEFT_PARENTHESIS { + async = true + kind = ast.PropertyKindMethod + self.next() + } + } + generator := false + if self.token == token.MULTIPLY && (kind == "" || kind == ast.PropertyKindMethod) { + generator = true + kind = ast.PropertyKindMethod + self.next() + } + + _, keyName, value, tkn := self.parseObjectPropertyKey() + if value == nil { + continue + } + computed := tkn == token.ILLEGAL + _, private := value.(*ast.PrivateIdentifier) + + if static && !private && keyName == "prototype" { + self.error(value.Idx0(), "Classes may not have a static property named 'prototype'") + } + + if kind == "" && self.token == token.LEFT_PARENTHESIS { + kind = ast.PropertyKindMethod + } + + if kind != "" { + // method + if keyName == "constructor" && !computed { + if !static { + if kind != ast.PropertyKindMethod { + self.error(value.Idx0(), "Class constructor may not be an accessor") + } else if async { + self.error(value.Idx0(), "Class constructor may not be an async method") + } else if generator { + self.error(value.Idx0(), "Class constructor may not be a generator") + } + } else if private { + self.error(value.Idx0(), "Class constructor may not be a private method") + } + } + md := &ast.MethodDefinition{ + Idx: start, + Key: value, + Kind: kind, + Body: self.parseMethodDefinition(methodBodyStart, kind, generator, async), + Static: static, + Computed: computed, + } + node.Body = append(node.Body, md) + } else { + // field + isCtor := !computed && keyName == "constructor" + if !isCtor { + if name, ok := value.(*ast.PrivateIdentifier); ok { + isCtor = name.Name == "constructor" + } + } + if isCtor { + self.error(value.Idx0(), "Classes may not have a field named 'constructor'") + } + var initializer ast.Expression + if self.token == token.ASSIGN { + self.next() + initializer = self.parseExpression() + } + + if !self.implicitSemicolon && self.token != token.SEMICOLON && self.token != token.RIGHT_BRACE { + self.errorUnexpectedToken(self.token) + break + } + node.Body = append(node.Body, &ast.FieldDefinition{ + Idx: start, + Key: value, + Initializer: initializer, + Static: static, + Computed: computed, + }) + } + } + + node.RightBrace = self.expect(token.RIGHT_BRACE) + node.Source = self.slice(node.Class, node.RightBrace+1) + + return node +} + +func (self *_parser) parseDebuggerStatement() ast.Statement { + idx := self.expect(token.DEBUGGER) + + node := &ast.DebuggerStatement{ + Debugger: idx, + } + + self.semicolon() + + return node +} + +func (self *_parser) parseReturnStatement() ast.Statement { + idx := self.expect(token.RETURN) + + if !self.scope.inFunction { + self.error(idx, "Illegal return statement") + self.nextStatement() + return &ast.BadStatement{From: idx, To: self.idx} + } + + node := &ast.ReturnStatement{ + Return: idx, + } + + if !self.implicitSemicolon && self.token != token.SEMICOLON && self.token != token.RIGHT_BRACE && self.token != token.EOF { + node.Argument = self.parseExpression() + } + + self.semicolon() + + return node +} + +func (self *_parser) parseThrowStatement() ast.Statement { + idx := self.expect(token.THROW) + + if self.implicitSemicolon { + if self.chr == -1 { // Hackish + self.error(idx, "Unexpected end of input") + } else { + self.error(idx, "Illegal newline after throw") + } + self.nextStatement() + return &ast.BadStatement{From: idx, To: self.idx} + } + + node := &ast.ThrowStatement{ + Throw: idx, + Argument: self.parseExpression(), + } + + self.semicolon() + + return node +} + +func (self *_parser) parseSwitchStatement() ast.Statement { + idx := self.expect(token.SWITCH) + self.expect(token.LEFT_PARENTHESIS) + node := &ast.SwitchStatement{ + Switch: idx, + Discriminant: self.parseExpression(), + Default: -1, + } + self.expect(token.RIGHT_PARENTHESIS) + + self.expect(token.LEFT_BRACE) + + inSwitch := self.scope.inSwitch + self.scope.inSwitch = true + defer func() { + self.scope.inSwitch = inSwitch + }() + + for index := 0; self.token != token.EOF; index++ { + if self.token == token.RIGHT_BRACE { + node.RightBrace = self.idx + self.next() + break + } + + clause := self.parseCaseStatement() + if clause.Test == nil { + if node.Default != -1 { + self.error(clause.Case, "Already saw a default in switch") + } + node.Default = index + } + node.Body = append(node.Body, clause) + } + + return node +} + +func (self *_parser) parseWithStatement() ast.Statement { + node := &ast.WithStatement{} + node.With = self.expect(token.WITH) + self.expect(token.LEFT_PARENTHESIS) + node.Object = self.parseExpression() + self.expect(token.RIGHT_PARENTHESIS) + self.scope.allowLet = false + node.Body = self.parseStatement() + + return node +} + +func (self *_parser) parseCaseStatement() *ast.CaseStatement { + + node := &ast.CaseStatement{ + Case: self.idx, + } + if self.token == token.DEFAULT { + self.next() + } else { + self.expect(token.CASE) + node.Test = self.parseExpression() + } + self.expect(token.COLON) + + for { + if self.token == token.EOF || + self.token == token.RIGHT_BRACE || + self.token == token.CASE || + self.token == token.DEFAULT { + break + } + self.scope.allowLet = true + node.Consequent = append(node.Consequent, self.parseStatement()) + + } + + return node +} + +func (self *_parser) parseIterationStatement() ast.Statement { + inIteration := self.scope.inIteration + self.scope.inIteration = true + defer func() { + self.scope.inIteration = inIteration + }() + self.scope.allowLet = false + return self.parseStatement() +} + +func (self *_parser) parseForIn(idx file.Idx, into ast.ForInto) *ast.ForInStatement { + + // Already have consumed " in" + + source := self.parseExpression() + self.expect(token.RIGHT_PARENTHESIS) + + return &ast.ForInStatement{ + For: idx, + Into: into, + Source: source, + Body: self.parseIterationStatement(), + } +} + +func (self *_parser) parseForOf(idx file.Idx, into ast.ForInto) *ast.ForOfStatement { + + // Already have consumed " of" + + source := self.parseAssignmentExpression() + self.expect(token.RIGHT_PARENTHESIS) + + return &ast.ForOfStatement{ + For: idx, + Into: into, + Source: source, + Body: self.parseIterationStatement(), + } +} + +func (self *_parser) parseFor(idx file.Idx, initializer ast.ForLoopInitializer) *ast.ForStatement { + + // Already have consumed " ;" + + var test, update ast.Expression + + if self.token != token.SEMICOLON { + test = self.parseExpression() + } + self.expect(token.SEMICOLON) + + if self.token != token.RIGHT_PARENTHESIS { + update = self.parseExpression() + } + self.expect(token.RIGHT_PARENTHESIS) + + return &ast.ForStatement{ + For: idx, + Initializer: initializer, + Test: test, + Update: update, + Body: self.parseIterationStatement(), + } +} + +func (self *_parser) parseForOrForInStatement() ast.Statement { + idx := self.expect(token.FOR) + self.expect(token.LEFT_PARENTHESIS) + + var initializer ast.ForLoopInitializer + + forIn := false + forOf := false + var into ast.ForInto + if self.token != token.SEMICOLON { + + allowIn := self.scope.allowIn + self.scope.allowIn = false + tok := self.token + if tok == token.LET { + switch self.peek() { + case token.IDENTIFIER, token.LEFT_BRACKET, token.LEFT_BRACE: + default: + tok = token.IDENTIFIER + } + } + if tok == token.VAR || tok == token.LET || tok == token.CONST { + idx := self.idx + self.next() + var list []*ast.Binding + if tok == token.VAR { + list = self.parseVarDeclarationList(idx) + } else { + list = self.parseVariableDeclarationList() + } + if len(list) == 1 { + if self.token == token.IN { + self.next() // in + forIn = true + } else if self.token == token.IDENTIFIER && self.literal == "of" { + self.next() + forOf = true + } + } + if forIn || forOf { + if list[0].Initializer != nil { + self.error(list[0].Initializer.Idx0(), "for-in loop variable declaration may not have an initializer") + } + if tok == token.VAR { + into = &ast.ForIntoVar{ + Binding: list[0], + } + } else { + into = &ast.ForDeclaration{ + Idx: idx, + IsConst: tok == token.CONST, + Target: list[0].Target, + } + } + } else { + self.ensurePatternInit(list) + if tok == token.VAR { + initializer = &ast.ForLoopInitializerVarDeclList{ + List: list, + } + } else { + initializer = &ast.ForLoopInitializerLexicalDecl{ + LexicalDeclaration: ast.LexicalDeclaration{ + Idx: idx, + Token: tok, + List: list, + }, + } + } + } + } else { + expr := self.parseExpression() + if self.token == token.IN { + self.next() + forIn = true + } else if self.token == token.IDENTIFIER && self.literal == "of" { + self.next() + forOf = true + } + if forIn || forOf { + switch e := expr.(type) { + case *ast.Identifier, *ast.DotExpression, *ast.PrivateDotExpression, *ast.BracketExpression, *ast.Binding: + // These are all acceptable + case *ast.ObjectLiteral: + expr = self.reinterpretAsObjectAssignmentPattern(e) + case *ast.ArrayLiteral: + expr = self.reinterpretAsArrayAssignmentPattern(e) + default: + self.error(idx, "Invalid left-hand side in for-in or for-of") + self.nextStatement() + return &ast.BadStatement{From: idx, To: self.idx} + } + into = &ast.ForIntoExpression{ + Expression: expr, + } + } else { + initializer = &ast.ForLoopInitializerExpression{ + Expression: expr, + } + } + } + self.scope.allowIn = allowIn + } + + if forIn { + return self.parseForIn(idx, into) + } + if forOf { + return self.parseForOf(idx, into) + } + + self.expect(token.SEMICOLON) + return self.parseFor(idx, initializer) +} + +func (self *_parser) ensurePatternInit(list []*ast.Binding) { + for _, item := range list { + if _, ok := item.Target.(ast.Pattern); ok { + if item.Initializer == nil { + self.error(item.Idx1(), "Missing initializer in destructuring declaration") + break + } + } + } +} + +func (self *_parser) parseVariableStatement() *ast.VariableStatement { + + idx := self.expect(token.VAR) + + list := self.parseVarDeclarationList(idx) + self.ensurePatternInit(list) + self.semicolon() + + return &ast.VariableStatement{ + Var: idx, + List: list, + } +} + +func (self *_parser) parseLexicalDeclaration(tok token.Token) *ast.LexicalDeclaration { + idx := self.expect(tok) + if !self.scope.allowLet { + self.error(idx, "Lexical declaration cannot appear in a single-statement context") + } + + list := self.parseVariableDeclarationList() + self.ensurePatternInit(list) + self.semicolon() + + return &ast.LexicalDeclaration{ + Idx: idx, + Token: tok, + List: list, + } +} + +func (self *_parser) parseDoWhileStatement() ast.Statement { + inIteration := self.scope.inIteration + self.scope.inIteration = true + defer func() { + self.scope.inIteration = inIteration + }() + + node := &ast.DoWhileStatement{} + node.Do = self.expect(token.DO) + if self.token == token.LEFT_BRACE { + node.Body = self.parseBlockStatement() + } else { + self.scope.allowLet = false + node.Body = self.parseStatement() + } + + self.expect(token.WHILE) + self.expect(token.LEFT_PARENTHESIS) + node.Test = self.parseExpression() + node.RightParenthesis = self.expect(token.RIGHT_PARENTHESIS) + if self.token == token.SEMICOLON { + self.next() + } + + return node +} + +func (self *_parser) parseWhileStatement() ast.Statement { + idx := self.expect(token.WHILE) + self.expect(token.LEFT_PARENTHESIS) + node := &ast.WhileStatement{ + While: idx, + Test: self.parseExpression(), + } + self.expect(token.RIGHT_PARENTHESIS) + node.Body = self.parseIterationStatement() + + return node +} + +func (self *_parser) parseIfStatement() ast.Statement { + self.expect(token.IF) + self.expect(token.LEFT_PARENTHESIS) + node := &ast.IfStatement{ + Test: self.parseExpression(), + } + self.expect(token.RIGHT_PARENTHESIS) + + if self.token == token.LEFT_BRACE { + node.Consequent = self.parseBlockStatement() + } else { + self.scope.allowLet = false + node.Consequent = self.parseStatement() + } + + if self.token == token.ELSE { + self.next() + self.scope.allowLet = false + node.Alternate = self.parseStatement() + } + + return node +} + +func (self *_parser) parseSourceElements() (body []ast.Statement) { + for self.token != token.EOF { + self.scope.allowLet = true + body = append(body, self.parseStatement()) + } + + return body +} + +func (self *_parser) parseProgram() *ast.Program { + prg := &ast.Program{ + Body: self.parseSourceElements(), + DeclarationList: self.scope.declarationList, + File: self.file, + } + self.file.SetSourceMap(self.parseSourceMap()) + return prg +} + +func extractSourceMapLine(str string) string { + for { + p := strings.LastIndexByte(str, '\n') + line := str[p+1:] + if line != "" && line != "})" { + if strings.HasPrefix(line, "//# sourceMappingURL=") { + return line + } + break + } + if p >= 0 { + str = str[:p] + } else { + break + } + } + return "" +} + +func (self *_parser) parseSourceMap() *sourcemap.Consumer { + if self.opts.disableSourceMaps { + return nil + } + if smLine := extractSourceMapLine(self.str); smLine != "" { + urlIndex := strings.Index(smLine, "=") + urlStr := smLine[urlIndex+1:] + + var data []byte + var err error + if strings.HasPrefix(urlStr, "data:application/json") { + b64Index := strings.Index(urlStr, ",") + b64 := urlStr[b64Index+1:] + data, err = base64.StdEncoding.DecodeString(b64) + } else { + if sourceURL := file.ResolveSourcemapURL(self.file.Name(), urlStr); sourceURL != nil { + if self.opts.sourceMapLoader != nil { + data, err = self.opts.sourceMapLoader(sourceURL.String()) + } else { + if sourceURL.Scheme == "" || sourceURL.Scheme == "file" { + data, err = os.ReadFile(sourceURL.Path) + } else { + err = fmt.Errorf("unsupported source map URL scheme: %s", sourceURL.Scheme) + } + } + } + } + + if err != nil { + self.error(file.Idx(0), "Could not load source map: %v", err) + return nil + } + if data == nil { + return nil + } + + if sm, err := sourcemap.Parse(self.file.Name(), data); err == nil { + return sm + } else { + self.error(file.Idx(0), "Could not parse source map: %v", err) + } + } + return nil +} + +func (self *_parser) parseBreakStatement() ast.Statement { + idx := self.expect(token.BREAK) + semicolon := self.implicitSemicolon + if self.token == token.SEMICOLON { + semicolon = true + self.next() + } + + if semicolon || self.token == token.RIGHT_BRACE { + self.implicitSemicolon = false + if !self.scope.inIteration && !self.scope.inSwitch { + goto illegal + } + return &ast.BranchStatement{ + Idx: idx, + Token: token.BREAK, + } + } + + self.tokenToBindingId() + if self.token == token.IDENTIFIER { + identifier := self.parseIdentifier() + if !self.scope.hasLabel(identifier.Name) { + self.error(idx, "Undefined label '%s'", identifier.Name) + return &ast.BadStatement{From: idx, To: identifier.Idx1()} + } + self.semicolon() + return &ast.BranchStatement{ + Idx: idx, + Token: token.BREAK, + Label: identifier, + } + } + + self.expect(token.IDENTIFIER) + +illegal: + self.error(idx, "Illegal break statement") + self.nextStatement() + return &ast.BadStatement{From: idx, To: self.idx} +} + +func (self *_parser) parseContinueStatement() ast.Statement { + idx := self.expect(token.CONTINUE) + semicolon := self.implicitSemicolon + if self.token == token.SEMICOLON { + semicolon = true + self.next() + } + + if semicolon || self.token == token.RIGHT_BRACE { + self.implicitSemicolon = false + if !self.scope.inIteration { + goto illegal + } + return &ast.BranchStatement{ + Idx: idx, + Token: token.CONTINUE, + } + } + + self.tokenToBindingId() + if self.token == token.IDENTIFIER { + identifier := self.parseIdentifier() + if !self.scope.hasLabel(identifier.Name) { + self.error(idx, "Undefined label '%s'", identifier.Name) + return &ast.BadStatement{From: idx, To: identifier.Idx1()} + } + if !self.scope.inIteration { + goto illegal + } + self.semicolon() + return &ast.BranchStatement{ + Idx: idx, + Token: token.CONTINUE, + Label: identifier, + } + } + + self.expect(token.IDENTIFIER) + +illegal: + self.error(idx, "Illegal continue statement") + self.nextStatement() + return &ast.BadStatement{From: idx, To: self.idx} +} + +// Find the next statement after an error (recover) +func (self *_parser) nextStatement() { + for { + switch self.token { + case token.BREAK, token.CONTINUE, + token.FOR, token.IF, token.RETURN, token.SWITCH, + token.VAR, token.DO, token.TRY, token.WITH, + token.WHILE, token.THROW, token.CATCH, token.FINALLY: + // Return only if parser made some progress since last + // sync or if it has not reached 10 next calls without + // progress. Otherwise consume at least one token to + // avoid an endless parser loop + if self.idx == self.recover.idx && self.recover.count < 10 { + self.recover.count++ + return + } + if self.idx > self.recover.idx { + self.recover.idx = self.idx + self.recover.count = 0 + return + } + // Reaching here indicates a parser bug, likely an + // incorrect token list in this function, but it only + // leads to skipping of possibly correct code if a + // previous error is present, and thus is preferred + // over a non-terminating parse. + case token.EOF: + return + } + self.next() + } +} diff --git a/goja/parser/testutil_test.go b/goja/parser/testutil_test.go new file mode 100644 index 0000000..5dc9704 --- /dev/null +++ b/goja/parser/testutil_test.go @@ -0,0 +1,49 @@ +package parser + +import ( + "fmt" + "path/filepath" + "runtime" + "testing" +) + +// Quick and dirty replacement for terst + +func tt(t *testing.T, f func()) { + defer func() { + if x := recover(); x != nil { + pcs := make([]uintptr, 16) + pcs = pcs[:runtime.Callers(1, pcs)] + frames := runtime.CallersFrames(pcs) + var file string + var line int + for { + frame, more := frames.Next() + // The line number here must match the line where f() is called (see below) + if frame.Line == 40 && filepath.Base(frame.File) == "testutil_test.go" { + break + } + + if !more { + break + } + file, line = frame.File, frame.Line + } + if line > 0 { + t.Errorf("Error at %s:%d: %v", filepath.Base(file), line, x) + } else { + t.Errorf("Error at : %v", x) + } + } + }() + + f() +} + +func is(a, b interface{}) { + as := fmt.Sprintf("%v", a) + bs := fmt.Sprintf("%v", b) + if as != bs { + panic(fmt.Errorf("%+v(%T) != %+v(%T)", a, a, b, b)) + } +} diff --git a/goja/profiler.go b/goja/profiler.go new file mode 100644 index 0000000..3d21ad1 --- /dev/null +++ b/goja/profiler.go @@ -0,0 +1,350 @@ +package goja + +import ( + "errors" + "io" + "strconv" + "sync" + "sync/atomic" + "time" + + "github.com/google/pprof/profile" +) + +const profInterval = 10 * time.Millisecond +const profMaxStackDepth = 64 + +const ( + profReqNone int32 = iota + profReqDoSample + profReqSampleReady + profReqStop +) + +type _globalProfiler struct { + p profiler + w io.Writer + + enabled int32 +} + +var globalProfiler _globalProfiler + +type profTracker struct { + req, finished int32 + start, stop time.Time + numFrames int + frames [profMaxStackDepth]StackFrame +} + +type profiler struct { + mu sync.Mutex + trackers []*profTracker + buf *profBuffer + running bool +} + +type profFunc struct { + f profile.Function + locs map[int32]*profile.Location +} + +type profSampleNode struct { + loc *profile.Location + sample *profile.Sample + parent *profSampleNode + children map[*profile.Location]*profSampleNode +} + +type profBuffer struct { + funcs map[*Program]*profFunc + root profSampleNode +} + +func (pb *profBuffer) addSample(pt *profTracker) { + sampleFrames := pt.frames[:pt.numFrames] + n := &pb.root + for j := len(sampleFrames) - 1; j >= 0; j-- { + frame := sampleFrames[j] + if frame.prg == nil { + continue + } + var f *profFunc + if f = pb.funcs[frame.prg]; f == nil { + f = &profFunc{ + locs: make(map[int32]*profile.Location), + } + if pb.funcs == nil { + pb.funcs = make(map[*Program]*profFunc) + } + pb.funcs[frame.prg] = f + } + var loc *profile.Location + if loc = f.locs[int32(frame.pc)]; loc == nil { + loc = &profile.Location{} + f.locs[int32(frame.pc)] = loc + } + if nn := n.children[loc]; nn == nil { + if n.children == nil { + n.children = make(map[*profile.Location]*profSampleNode, 1) + } + nn = &profSampleNode{ + parent: n, + loc: loc, + } + n.children[loc] = nn + n = nn + } else { + n = nn + } + } + smpl := n.sample + if smpl == nil { + locs := make([]*profile.Location, 0, len(sampleFrames)) + for n1 := n; n1.loc != nil; n1 = n1.parent { + locs = append(locs, n1.loc) + } + smpl = &profile.Sample{ + Location: locs, + Value: make([]int64, 2), + } + n.sample = smpl + } + smpl.Value[0]++ + smpl.Value[1] += int64(pt.stop.Sub(pt.start)) +} + +func (pb *profBuffer) profile() *profile.Profile { + pr := profile.Profile{} + pr.SampleType = []*profile.ValueType{ + {Type: "samples", Unit: "count"}, + {Type: "cpu", Unit: "nanoseconds"}, + } + pr.PeriodType = pr.SampleType[1] + pr.Period = int64(profInterval) + mapping := &profile.Mapping{ + ID: 1, + File: "[ECMAScript code]", + } + pr.Mapping = make([]*profile.Mapping, 1, len(pb.funcs)+1) + pr.Mapping[0] = mapping + + pr.Function = make([]*profile.Function, 0, len(pb.funcs)) + funcNames := make(map[string]struct{}) + var funcId, locId uint64 + for prg, f := range pb.funcs { + fileName := prg.src.Name() + funcId++ + f.f.ID = funcId + f.f.Filename = fileName + var funcName string + if prg.funcName != "" { + funcName = prg.funcName.String() + } else { + funcName = "" + } + // Make sure the function name is unique, otherwise the graph display merges them into one node, even + // if they are in different mappings. + if _, exists := funcNames[funcName]; exists { + funcName += "." + strconv.FormatUint(f.f.ID, 10) + } else { + funcNames[funcName] = struct{}{} + } + f.f.Name = funcName + pr.Function = append(pr.Function, &f.f) + for pc, loc := range f.locs { + locId++ + loc.ID = locId + pos := prg.src.Position(prg.sourceOffset(int(pc))) + loc.Line = []profile.Line{ + { + Function: &f.f, + Line: int64(pos.Line), + }, + } + + loc.Mapping = mapping + pr.Location = append(pr.Location, loc) + } + } + pb.addSamples(&pr, &pb.root) + return &pr +} + +func (pb *profBuffer) addSamples(p *profile.Profile, n *profSampleNode) { + if n.sample != nil { + p.Sample = append(p.Sample, n.sample) + } + for _, child := range n.children { + pb.addSamples(p, child) + } +} + +func (p *profiler) run() { + ticker := time.NewTicker(profInterval) + counter := 0 + + for ts := range ticker.C { + p.mu.Lock() + left := len(p.trackers) + if left == 0 { + break + } + for { + // This loop runs until either one of the VMs is signalled or all of the VMs are scanned and found + // busy or deleted. + if counter >= len(p.trackers) { + counter = 0 + } + tracker := p.trackers[counter] + req := atomic.LoadInt32(&tracker.req) + if req == profReqSampleReady { + p.buf.addSample(tracker) + } + if atomic.LoadInt32(&tracker.finished) != 0 { + p.trackers[counter] = p.trackers[len(p.trackers)-1] + p.trackers[len(p.trackers)-1] = nil + p.trackers = p.trackers[:len(p.trackers)-1] + } else { + counter++ + if req != profReqDoSample { + // signal the VM to take a sample + tracker.start = ts + atomic.StoreInt32(&tracker.req, profReqDoSample) + break + } + } + left-- + if left <= 0 { + // all VMs are busy + break + } + } + p.mu.Unlock() + } + ticker.Stop() + p.running = false + p.mu.Unlock() +} + +func (p *profiler) registerVm() *profTracker { + pt := new(profTracker) + p.mu.Lock() + if p.buf != nil { + p.trackers = append(p.trackers, pt) + if !p.running { + go p.run() + p.running = true + } + } else { + pt.req = profReqStop + } + p.mu.Unlock() + return pt +} + +func (p *profiler) start() error { + p.mu.Lock() + if p.buf != nil { + p.mu.Unlock() + return errors.New("profiler is already active") + } + p.buf = new(profBuffer) + p.mu.Unlock() + return nil +} + +func (p *profiler) stop() *profile.Profile { + p.mu.Lock() + trackers, buf := p.trackers, p.buf + p.trackers, p.buf = nil, nil + p.mu.Unlock() + if buf != nil { + k := 0 + for i, tracker := range trackers { + req := atomic.LoadInt32(&tracker.req) + if req == profReqSampleReady { + buf.addSample(tracker) + } else if req == profReqDoSample { + // In case the VM is requested to do a sample, there is a small chance of a race + // where we set profReqStop in between the read and the write, so that the req + // ends up being set to profReqSampleReady. It's no such a big deal if we do nothing, + // it just means the VM remains in tracing mode until it finishes the current run, + // but we do an extra cleanup step later just in case. + if i != k { + trackers[k] = trackers[i] + } + k++ + } + atomic.StoreInt32(&tracker.req, profReqStop) + } + + if k > 0 { + trackers = trackers[:k] + go func() { + // Make sure all VMs are requested to stop tracing. + for { + k := 0 + for i, tracker := range trackers { + req := atomic.LoadInt32(&tracker.req) + if req != profReqStop { + atomic.StoreInt32(&tracker.req, profReqStop) + if i != k { + trackers[k] = trackers[i] + } + k++ + } + } + + if k == 0 { + return + } + trackers = trackers[:k] + time.Sleep(100 * time.Millisecond) + } + }() + } + return buf.profile() + } + return nil +} + +/* +StartProfile enables execution time profiling for all Runtimes within the current process. +This works similar to pprof.StartCPUProfile and produces the same format which can be consumed by `go tool pprof`. +There are, however, a few notable differences. Firstly, it's not a CPU profile, rather "execution time" profile. +It measures the time the VM spends executing an instruction. If this instruction happens to be a call to a +blocking Go function, the waiting time will be measured. Secondly, the 'cpu' sample isn't simply `count*period`, +it's the time interval between when sampling was requested and when the instruction has finished. If a VM is still +executing the same instruction when the time comes for the next sample, the sampling is skipped (i.e. `count` doesn't +grow). + +If there are multiple functions with the same name, their names get a '.N' suffix, where N is a unique number, +because otherwise the graph view merges them together (even if they are in different mappings). This includes +"" functions. + +The sampling period is set to 10ms. + +It returns an error if profiling is already active. +*/ +func StartProfile(w io.Writer) error { + err := globalProfiler.p.start() + if err != nil { + return err + } + globalProfiler.w = w + atomic.StoreInt32(&globalProfiler.enabled, 1) + return nil +} + +/* +StopProfile stops the current profile initiated by StartProfile, if any. +*/ +func StopProfile() { + atomic.StoreInt32(&globalProfiler.enabled, 0) + pr := globalProfiler.p.stop() + if pr != nil { + _ = pr.Write(globalProfiler.w) + } + globalProfiler.w = nil +} diff --git a/goja/profiler_test.go b/goja/profiler_test.go new file mode 100644 index 0000000..99478e6 --- /dev/null +++ b/goja/profiler_test.go @@ -0,0 +1,102 @@ +package goja + +import ( + "sync/atomic" + "testing" + "time" +) + +func TestProfiler(t *testing.T) { + + err := StartProfile(nil) + if err != nil { + t.Fatal(err) + } + + vm := New() + go func() { + _, err := vm.RunScript("test123.js", ` + const a = 2 + 2; + function loop() { + for(;;) {} + } + loop(); + `) + if err != nil { + if _, ok := err.(*InterruptedError); !ok { + panic(err) + } + } + }() + + time.Sleep(200 * time.Millisecond) + + atomic.StoreInt32(&globalProfiler.enabled, 0) + pr := globalProfiler.p.stop() + + if len(pr.Sample) == 0 { + t.Fatal("No samples were recorded") + } + + var running bool + for i := 0; i < 10; i++ { + time.Sleep(10 * time.Millisecond) + globalProfiler.p.mu.Lock() + running = globalProfiler.p.running + globalProfiler.p.mu.Unlock() + if !running { + break + } + } + if running { + t.Fatal("The profiler is still running") + } + vm.Interrupt(nil) +} + +func TestProfiler1(t *testing.T) { + t.Skip("This test takes too long with race detector enabled and is non-deterministic. It's left here mostly for documentation purposes.") + + err := StartProfile(nil) + if err != nil { + t.Fatal(err) + } + + go func() { + sleep := func() { + time.Sleep(1 * time.Second) + } + // Spawn new vms faster than once every 10ms (the profiler interval) and make sure they don't finish too soon. + // This means (*profiler).run() won't be fast enough to collect the samples, so they must be collected + // after the profiler is stopped. + for i := 0; i < 500; i++ { + go func() { + vm := New() + vm.Set("sleep", sleep) + _, err := vm.RunScript("test123.js", ` + function loop() { + for (let i = 0; i < 50000; i++) { + const a = Math.pow(Math.Pi, Math.Pi); + } + } + loop(); + sleep(); + `) + if err != nil { + if _, ok := err.(*InterruptedError); !ok { + panic(err) + } + } + }() + time.Sleep(1 * time.Millisecond) + } + }() + + time.Sleep(500 * time.Millisecond) + atomic.StoreInt32(&globalProfiler.enabled, 0) + pr := globalProfiler.p.stop() + + if len(pr.Sample) == 0 { + t.Fatal("No samples were recorded") + } +} diff --git a/goja/proxy.go b/goja/proxy.go new file mode 100644 index 0000000..e9bd8c9 --- /dev/null +++ b/goja/proxy.go @@ -0,0 +1,1074 @@ +package goja + +import ( + "fmt" + "reflect" + + "github.com/dop251/goja/unistring" +) + +// Proxy is a Go wrapper around ECMAScript Proxy. Calling Runtime.ToValue() on it +// returns the underlying Proxy. Calling Export() on an ECMAScript Proxy returns a wrapper. +// Use Runtime.NewProxy() to create one. +type Proxy struct { + proxy *proxyObject +} + +var ( + proxyType = reflect.TypeOf(Proxy{}) +) + +type proxyPropIter struct { + p *proxyObject + names []Value + idx int +} + +func (i *proxyPropIter) next() (propIterItem, iterNextFunc) { + for i.idx < len(i.names) { + name := i.names[i.idx] + i.idx++ + return propIterItem{name: name}, i.next + } + return propIterItem{}, nil +} + +func (r *Runtime) newProxyObject(target, handler, proto *Object) *proxyObject { + return r._newProxyObject(target, &jsProxyHandler{handler: handler}, proto) +} + +func (r *Runtime) _newProxyObject(target *Object, handler proxyHandler, proto *Object) *proxyObject { + v := &Object{runtime: r} + p := &proxyObject{} + v.self = p + p.val = v + p.class = classObject + if proto == nil { + p.prototype = r.global.ObjectPrototype + } else { + p.prototype = proto + } + p.extensible = false + p.init() + p.target = target + p.handler = handler + if call, ok := target.self.assertCallable(); ok { + p.call = call + } + if ctor := target.self.assertConstructor(); ctor != nil { + p.ctor = ctor + } + return p +} + +func (p Proxy) Revoke() { + p.proxy.revoke() +} + +func (p Proxy) Handler() *Object { + if handler := p.proxy.handler; handler != nil { + return handler.toObject(p.proxy.val.runtime) + } + return nil +} + +func (p Proxy) Target() *Object { + return p.proxy.target +} + +func (p Proxy) toValue(r *Runtime) Value { + if p.proxy == nil { + return _null + } + proxy := p.proxy.val + if proxy.runtime != r { + panic(r.NewTypeError("Illegal runtime transition of a Proxy")) + } + return proxy +} + +type proxyTrap string + +const ( + proxy_trap_getPrototypeOf = "getPrototypeOf" + proxy_trap_setPrototypeOf = "setPrototypeOf" + proxy_trap_isExtensible = "isExtensible" + proxy_trap_preventExtensions = "preventExtensions" + proxy_trap_getOwnPropertyDescriptor = "getOwnPropertyDescriptor" + proxy_trap_defineProperty = "defineProperty" + proxy_trap_has = "has" + proxy_trap_get = "get" + proxy_trap_set = "set" + proxy_trap_deleteProperty = "deleteProperty" + proxy_trap_ownKeys = "ownKeys" + proxy_trap_apply = "apply" + proxy_trap_construct = "construct" +) + +func (p proxyTrap) String() (name string) { + return string(p) +} + +type proxyHandler interface { + getPrototypeOf(target *Object) (Value, bool) + setPrototypeOf(target *Object, proto *Object) (bool, bool) + isExtensible(target *Object) (bool, bool) + preventExtensions(target *Object) (bool, bool) + + getOwnPropertyDescriptorStr(target *Object, prop unistring.String) (Value, bool) + getOwnPropertyDescriptorIdx(target *Object, prop valueInt) (Value, bool) + getOwnPropertyDescriptorSym(target *Object, prop *Symbol) (Value, bool) + + definePropertyStr(target *Object, prop unistring.String, desc PropertyDescriptor) (bool, bool) + definePropertyIdx(target *Object, prop valueInt, desc PropertyDescriptor) (bool, bool) + definePropertySym(target *Object, prop *Symbol, desc PropertyDescriptor) (bool, bool) + + hasStr(target *Object, prop unistring.String) (bool, bool) + hasIdx(target *Object, prop valueInt) (bool, bool) + hasSym(target *Object, prop *Symbol) (bool, bool) + + getStr(target *Object, prop unistring.String, receiver Value) (Value, bool) + getIdx(target *Object, prop valueInt, receiver Value) (Value, bool) + getSym(target *Object, prop *Symbol, receiver Value) (Value, bool) + + setStr(target *Object, prop unistring.String, value Value, receiver Value) (bool, bool) + setIdx(target *Object, prop valueInt, value Value, receiver Value) (bool, bool) + setSym(target *Object, prop *Symbol, value Value, receiver Value) (bool, bool) + + deleteStr(target *Object, prop unistring.String) (bool, bool) + deleteIdx(target *Object, prop valueInt) (bool, bool) + deleteSym(target *Object, prop *Symbol) (bool, bool) + + ownKeys(target *Object) (*Object, bool) + apply(target *Object, this Value, args []Value) (Value, bool) + construct(target *Object, args []Value, newTarget *Object) (Value, bool) + + toObject(*Runtime) *Object +} + +type jsProxyHandler struct { + handler *Object +} + +func (h *jsProxyHandler) toObject(*Runtime) *Object { + return h.handler +} + +func (h *jsProxyHandler) proxyCall(trap proxyTrap, args ...Value) (Value, bool) { + r := h.handler.runtime + + if m := toMethod(r.getVStr(h.handler, unistring.String(trap.String()))); m != nil { + return m(FunctionCall{ + This: h.handler, + Arguments: args, + }), true + } + + return nil, false +} + +func (h *jsProxyHandler) boolProxyCall(trap proxyTrap, args ...Value) (bool, bool) { + if v, ok := h.proxyCall(trap, args...); ok { + return v.ToBoolean(), true + } + return false, false +} + +func (h *jsProxyHandler) getPrototypeOf(target *Object) (Value, bool) { + return h.proxyCall(proxy_trap_getPrototypeOf, target) +} + +func (h *jsProxyHandler) setPrototypeOf(target *Object, proto *Object) (bool, bool) { + var protoVal Value + if proto != nil { + protoVal = proto + } else { + protoVal = _null + } + return h.boolProxyCall(proxy_trap_setPrototypeOf, target, protoVal) +} + +func (h *jsProxyHandler) isExtensible(target *Object) (bool, bool) { + return h.boolProxyCall(proxy_trap_isExtensible, target) +} + +func (h *jsProxyHandler) preventExtensions(target *Object) (bool, bool) { + return h.boolProxyCall(proxy_trap_preventExtensions, target) +} + +func (h *jsProxyHandler) getOwnPropertyDescriptorStr(target *Object, prop unistring.String) (Value, bool) { + return h.proxyCall(proxy_trap_getOwnPropertyDescriptor, target, stringValueFromRaw(prop)) +} + +func (h *jsProxyHandler) getOwnPropertyDescriptorIdx(target *Object, prop valueInt) (Value, bool) { + return h.proxyCall(proxy_trap_getOwnPropertyDescriptor, target, prop.toString()) +} + +func (h *jsProxyHandler) getOwnPropertyDescriptorSym(target *Object, prop *Symbol) (Value, bool) { + return h.proxyCall(proxy_trap_getOwnPropertyDescriptor, target, prop) +} + +func (h *jsProxyHandler) definePropertyStr(target *Object, prop unistring.String, desc PropertyDescriptor) (bool, bool) { + return h.boolProxyCall(proxy_trap_defineProperty, target, stringValueFromRaw(prop), desc.toValue(h.handler.runtime)) +} + +func (h *jsProxyHandler) definePropertyIdx(target *Object, prop valueInt, desc PropertyDescriptor) (bool, bool) { + return h.boolProxyCall(proxy_trap_defineProperty, target, prop.toString(), desc.toValue(h.handler.runtime)) +} + +func (h *jsProxyHandler) definePropertySym(target *Object, prop *Symbol, desc PropertyDescriptor) (bool, bool) { + return h.boolProxyCall(proxy_trap_defineProperty, target, prop, desc.toValue(h.handler.runtime)) +} + +func (h *jsProxyHandler) hasStr(target *Object, prop unistring.String) (bool, bool) { + return h.boolProxyCall(proxy_trap_has, target, stringValueFromRaw(prop)) +} + +func (h *jsProxyHandler) hasIdx(target *Object, prop valueInt) (bool, bool) { + return h.boolProxyCall(proxy_trap_has, target, prop.toString()) +} + +func (h *jsProxyHandler) hasSym(target *Object, prop *Symbol) (bool, bool) { + return h.boolProxyCall(proxy_trap_has, target, prop) +} + +func (h *jsProxyHandler) getStr(target *Object, prop unistring.String, receiver Value) (Value, bool) { + return h.proxyCall(proxy_trap_get, target, stringValueFromRaw(prop), receiver) +} + +func (h *jsProxyHandler) getIdx(target *Object, prop valueInt, receiver Value) (Value, bool) { + return h.proxyCall(proxy_trap_get, target, prop.toString(), receiver) +} + +func (h *jsProxyHandler) getSym(target *Object, prop *Symbol, receiver Value) (Value, bool) { + return h.proxyCall(proxy_trap_get, target, prop, receiver) +} + +func (h *jsProxyHandler) setStr(target *Object, prop unistring.String, value Value, receiver Value) (bool, bool) { + return h.boolProxyCall(proxy_trap_set, target, stringValueFromRaw(prop), value, receiver) +} + +func (h *jsProxyHandler) setIdx(target *Object, prop valueInt, value Value, receiver Value) (bool, bool) { + return h.boolProxyCall(proxy_trap_set, target, prop.toString(), value, receiver) +} + +func (h *jsProxyHandler) setSym(target *Object, prop *Symbol, value Value, receiver Value) (bool, bool) { + return h.boolProxyCall(proxy_trap_set, target, prop, value, receiver) +} + +func (h *jsProxyHandler) deleteStr(target *Object, prop unistring.String) (bool, bool) { + return h.boolProxyCall(proxy_trap_deleteProperty, target, stringValueFromRaw(prop)) +} + +func (h *jsProxyHandler) deleteIdx(target *Object, prop valueInt) (bool, bool) { + return h.boolProxyCall(proxy_trap_deleteProperty, target, prop.toString()) +} + +func (h *jsProxyHandler) deleteSym(target *Object, prop *Symbol) (bool, bool) { + return h.boolProxyCall(proxy_trap_deleteProperty, target, prop) +} + +func (h *jsProxyHandler) ownKeys(target *Object) (*Object, bool) { + if v, ok := h.proxyCall(proxy_trap_ownKeys, target); ok { + return h.handler.runtime.toObject(v), true + } + return nil, false +} + +func (h *jsProxyHandler) apply(target *Object, this Value, args []Value) (Value, bool) { + return h.proxyCall(proxy_trap_apply, target, this, h.handler.runtime.newArrayValues(args)) +} + +func (h *jsProxyHandler) construct(target *Object, args []Value, newTarget *Object) (Value, bool) { + return h.proxyCall(proxy_trap_construct, target, h.handler.runtime.newArrayValues(args), newTarget) +} + +type proxyObject struct { + baseObject + target *Object + handler proxyHandler + call func(FunctionCall) Value + ctor func(args []Value, newTarget *Object) *Object +} + +func (p *proxyObject) checkHandler() proxyHandler { + r := p.val.runtime + if handler := p.handler; handler != nil { + return handler + } + panic(r.NewTypeError("Proxy already revoked")) +} + +func (p *proxyObject) proto() *Object { + target := p.target + if v, ok := p.checkHandler().getPrototypeOf(target); ok { + var handlerProto *Object + if v != _null { + handlerProto = p.val.runtime.toObject(v) + } + if !target.self.isExtensible() && !p.__sameValue(handlerProto, target.self.proto()) { + panic(p.val.runtime.NewTypeError("'getPrototypeOf' on proxy: proxy target is non-extensible but the trap did not return its actual prototype")) + } + return handlerProto + } + + return target.self.proto() +} + +func (p *proxyObject) setProto(proto *Object, throw bool) bool { + target := p.target + if v, ok := p.checkHandler().setPrototypeOf(target, proto); ok { + if v { + if !target.self.isExtensible() && !p.__sameValue(proto, target.self.proto()) { + panic(p.val.runtime.NewTypeError("'setPrototypeOf' on proxy: trap returned truish for setting a new prototype on the non-extensible proxy target")) + } + return true + } else { + p.val.runtime.typeErrorResult(throw, "'setPrototypeOf' on proxy: trap returned falsish") + return false + } + } + + return target.self.setProto(proto, throw) +} + +func (p *proxyObject) isExtensible() bool { + target := p.target + if booleanTrapResult, ok := p.checkHandler().isExtensible(p.target); ok { + if te := target.self.isExtensible(); booleanTrapResult != te { + panic(p.val.runtime.NewTypeError("'isExtensible' on proxy: trap result does not reflect extensibility of proxy target (which is '%v')", te)) + } + return booleanTrapResult + } + + return target.self.isExtensible() +} + +func (p *proxyObject) preventExtensions(throw bool) bool { + target := p.target + if booleanTrapResult, ok := p.checkHandler().preventExtensions(target); ok { + if !booleanTrapResult { + p.val.runtime.typeErrorResult(throw, "'preventExtensions' on proxy: trap returned falsish") + return false + } + if te := target.self.isExtensible(); booleanTrapResult && te { + panic(p.val.runtime.NewTypeError("'preventExtensions' on proxy: trap returned truish but the proxy target is extensible")) + } + } + + return target.self.preventExtensions(throw) +} + +func propToValueProp(v Value) *valueProperty { + if v == nil { + return nil + } + if v, ok := v.(*valueProperty); ok { + return v + } + return &valueProperty{ + value: v, + writable: true, + configurable: true, + enumerable: true, + } +} + +func (p *proxyObject) proxyDefineOwnPropertyPreCheck(trapResult, throw bool) bool { + if !trapResult { + p.val.runtime.typeErrorResult(throw, "'defineProperty' on proxy: trap returned falsish") + return false + } + return true +} + +func (p *proxyObject) proxyDefineOwnPropertyPostCheck(prop Value, target *Object, descr PropertyDescriptor) { + targetDesc := propToValueProp(prop) + extensibleTarget := target.self.isExtensible() + settingConfigFalse := descr.Configurable == FLAG_FALSE + if targetDesc == nil { + if !extensibleTarget { + panic(p.val.runtime.NewTypeError()) + } + if settingConfigFalse { + panic(p.val.runtime.NewTypeError()) + } + } else { + if !p.__isCompatibleDescriptor(extensibleTarget, &descr, targetDesc) { + panic(p.val.runtime.NewTypeError()) + } + if settingConfigFalse && targetDesc.configurable { + panic(p.val.runtime.NewTypeError()) + } + if targetDesc.value != nil && !targetDesc.configurable && targetDesc.writable { + if descr.Writable == FLAG_FALSE { + panic(p.val.runtime.NewTypeError()) + } + } + } +} + +func (p *proxyObject) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + target := p.target + if booleanTrapResult, ok := p.checkHandler().definePropertyStr(target, name, descr); ok { + if !p.proxyDefineOwnPropertyPreCheck(booleanTrapResult, throw) { + return false + } + p.proxyDefineOwnPropertyPostCheck(target.self.getOwnPropStr(name), target, descr) + return true + } + return target.self.defineOwnPropertyStr(name, descr, throw) +} + +func (p *proxyObject) defineOwnPropertyIdx(idx valueInt, descr PropertyDescriptor, throw bool) bool { + target := p.target + if booleanTrapResult, ok := p.checkHandler().definePropertyIdx(target, idx, descr); ok { + if !p.proxyDefineOwnPropertyPreCheck(booleanTrapResult, throw) { + return false + } + p.proxyDefineOwnPropertyPostCheck(target.self.getOwnPropIdx(idx), target, descr) + return true + } + + return target.self.defineOwnPropertyIdx(idx, descr, throw) +} + +func (p *proxyObject) defineOwnPropertySym(s *Symbol, descr PropertyDescriptor, throw bool) bool { + target := p.target + if booleanTrapResult, ok := p.checkHandler().definePropertySym(target, s, descr); ok { + if !p.proxyDefineOwnPropertyPreCheck(booleanTrapResult, throw) { + return false + } + p.proxyDefineOwnPropertyPostCheck(target.self.getOwnPropSym(s), target, descr) + return true + } + + return target.self.defineOwnPropertySym(s, descr, throw) +} + +func (p *proxyObject) proxyHasChecks(targetProp Value, target *Object, name fmt.Stringer) { + targetDesc := propToValueProp(targetProp) + if targetDesc != nil { + if !targetDesc.configurable { + panic(p.val.runtime.NewTypeError("'has' on proxy: trap returned falsish for property '%s' which exists in the proxy target as non-configurable", name.String())) + } + if !target.self.isExtensible() { + panic(p.val.runtime.NewTypeError("'has' on proxy: trap returned falsish for property '%s' but the proxy target is not extensible", name.String())) + } + } +} + +func (p *proxyObject) hasPropertyStr(name unistring.String) bool { + target := p.target + if b, ok := p.checkHandler().hasStr(target, name); ok { + if !b { + p.proxyHasChecks(target.self.getOwnPropStr(name), target, name) + } + return b + } + + return target.self.hasPropertyStr(name) +} + +func (p *proxyObject) hasPropertyIdx(idx valueInt) bool { + target := p.target + if b, ok := p.checkHandler().hasIdx(target, idx); ok { + if !b { + p.proxyHasChecks(target.self.getOwnPropIdx(idx), target, idx) + } + return b + } + + return target.self.hasPropertyIdx(idx) +} + +func (p *proxyObject) hasPropertySym(s *Symbol) bool { + target := p.target + if b, ok := p.checkHandler().hasSym(target, s); ok { + if !b { + p.proxyHasChecks(target.self.getOwnPropSym(s), target, s) + } + return b + } + + return target.self.hasPropertySym(s) +} + +func (p *proxyObject) hasOwnPropertyStr(name unistring.String) bool { + return p.getOwnPropStr(name) != nil +} + +func (p *proxyObject) hasOwnPropertyIdx(idx valueInt) bool { + return p.getOwnPropIdx(idx) != nil +} + +func (p *proxyObject) hasOwnPropertySym(s *Symbol) bool { + return p.getOwnPropSym(s) != nil +} + +func (p *proxyObject) proxyGetOwnPropertyDescriptor(targetProp Value, target *Object, trapResult Value, name fmt.Stringer) Value { + r := p.val.runtime + targetDesc := propToValueProp(targetProp) + var trapResultObj *Object + if trapResult != nil && trapResult != _undefined { + if obj, ok := trapResult.(*Object); ok { + trapResultObj = obj + } else { + panic(r.NewTypeError("'getOwnPropertyDescriptor' on proxy: trap returned neither object nor undefined for property '%s'", name.String())) + } + } + if trapResultObj == nil { + if targetDesc == nil { + return nil + } + if !targetDesc.configurable { + panic(r.NewTypeError()) + } + if !target.self.isExtensible() { + panic(r.NewTypeError()) + } + return nil + } + extensibleTarget := target.self.isExtensible() + resultDesc := r.toPropertyDescriptor(trapResultObj) + resultDesc.complete() + if !p.__isCompatibleDescriptor(extensibleTarget, &resultDesc, targetDesc) { + panic(r.NewTypeError("'getOwnPropertyDescriptor' on proxy: trap returned descriptor for property '%s' that is incompatible with the existing property in the proxy target", name.String())) + } + + if resultDesc.Configurable == FLAG_FALSE { + if targetDesc == nil { + panic(r.NewTypeError("'getOwnPropertyDescriptor' on proxy: trap reported non-configurability for property '%s' which is non-existent in the proxy target", name.String())) + } + + if targetDesc.configurable { + panic(r.NewTypeError("'getOwnPropertyDescriptor' on proxy: trap reported non-configurability for property '%s' which is configurable in the proxy target", name.String())) + } + + if resultDesc.Writable == FLAG_FALSE && targetDesc.writable { + panic(r.NewTypeError("'getOwnPropertyDescriptor' on proxy: trap reported non-configurable and writable for property '%s' which is non-configurable, non-writable in the proxy target", name.String())) + } + } + + if resultDesc.Writable == FLAG_TRUE && resultDesc.Configurable == FLAG_TRUE && + resultDesc.Enumerable == FLAG_TRUE { + return resultDesc.Value + } + return r.toValueProp(trapResultObj) +} + +func (p *proxyObject) getOwnPropStr(name unistring.String) Value { + target := p.target + if v, ok := p.checkHandler().getOwnPropertyDescriptorStr(target, name); ok { + return p.proxyGetOwnPropertyDescriptor(target.self.getOwnPropStr(name), target, v, name) + } + + return target.self.getOwnPropStr(name) +} + +func (p *proxyObject) getOwnPropIdx(idx valueInt) Value { + target := p.target + if v, ok := p.checkHandler().getOwnPropertyDescriptorIdx(target, idx); ok { + return p.proxyGetOwnPropertyDescriptor(target.self.getOwnPropIdx(idx), target, v, idx) + } + + return target.self.getOwnPropIdx(idx) +} + +func (p *proxyObject) getOwnPropSym(s *Symbol) Value { + target := p.target + if v, ok := p.checkHandler().getOwnPropertyDescriptorSym(target, s); ok { + return p.proxyGetOwnPropertyDescriptor(target.self.getOwnPropSym(s), target, v, s) + } + + return target.self.getOwnPropSym(s) +} + +func (p *proxyObject) proxyGetChecks(targetProp, trapResult Value, name fmt.Stringer) { + if targetDesc, ok := targetProp.(*valueProperty); ok { + if !targetDesc.accessor { + if !targetDesc.writable && !targetDesc.configurable && !trapResult.SameAs(targetDesc.value) { + panic(p.val.runtime.NewTypeError("'get' on proxy: property '%s' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected '%s' but got '%s')", name.String(), nilSafe(targetDesc.value), ret)) + } + } else { + if !targetDesc.configurable && targetDesc.getterFunc == nil && trapResult != _undefined { + panic(p.val.runtime.NewTypeError("'get' on proxy: property '%s' is a non-configurable accessor property on the proxy target and does not have a getter function, but the trap did not return 'undefined' (got '%s')", name.String(), ret)) + } + } + } +} + +func (p *proxyObject) getStr(name unistring.String, receiver Value) Value { + target := p.target + if receiver == nil { + receiver = p.val + } + if v, ok := p.checkHandler().getStr(target, name, receiver); ok { + p.proxyGetChecks(target.self.getOwnPropStr(name), v, name) + return v + } + return target.self.getStr(name, receiver) +} + +func (p *proxyObject) getIdx(idx valueInt, receiver Value) Value { + target := p.target + if receiver == nil { + receiver = p.val + } + if v, ok := p.checkHandler().getIdx(target, idx, receiver); ok { + p.proxyGetChecks(target.self.getOwnPropIdx(idx), v, idx) + return v + } + return target.self.getIdx(idx, receiver) +} + +func (p *proxyObject) getSym(s *Symbol, receiver Value) Value { + target := p.target + if receiver == nil { + receiver = p.val + } + if v, ok := p.checkHandler().getSym(target, s, receiver); ok { + p.proxyGetChecks(target.self.getOwnPropSym(s), v, s) + return v + } + + return target.self.getSym(s, receiver) +} + +func (p *proxyObject) proxySetPreCheck(trapResult, throw bool, name fmt.Stringer) bool { + if !trapResult { + p.val.runtime.typeErrorResult(throw, "'set' on proxy: trap returned falsish for property '%s'", name.String()) + } + return trapResult +} + +func (p *proxyObject) proxySetPostCheck(targetProp, value Value, name fmt.Stringer) { + if prop, ok := targetProp.(*valueProperty); ok { + if prop.accessor { + if !prop.configurable && prop.setterFunc == nil { + panic(p.val.runtime.NewTypeError("'set' on proxy: trap returned truish for property '%s' which exists in the proxy target as a non-configurable and non-writable accessor property without a setter", name.String())) + } + } else if !prop.configurable && !prop.writable && !p.__sameValue(prop.value, value) { + panic(p.val.runtime.NewTypeError("'set' on proxy: trap returned truish for property '%s' which exists in the proxy target as a non-configurable and non-writable data property with a different value", name.String())) + } + } +} + +func (p *proxyObject) proxySetStr(name unistring.String, value, receiver Value, throw bool) bool { + target := p.target + if v, ok := p.checkHandler().setStr(target, name, value, receiver); ok { + if p.proxySetPreCheck(v, throw, name) { + p.proxySetPostCheck(target.self.getOwnPropStr(name), value, name) + return true + } + return false + } + return target.setStr(name, value, receiver, throw) +} + +func (p *proxyObject) proxySetIdx(idx valueInt, value, receiver Value, throw bool) bool { + target := p.target + if v, ok := p.checkHandler().setIdx(target, idx, value, receiver); ok { + if p.proxySetPreCheck(v, throw, idx) { + p.proxySetPostCheck(target.self.getOwnPropIdx(idx), value, idx) + return true + } + return false + } + return target.setIdx(idx, value, receiver, throw) +} + +func (p *proxyObject) proxySetSym(s *Symbol, value, receiver Value, throw bool) bool { + target := p.target + if v, ok := p.checkHandler().setSym(target, s, value, receiver); ok { + if p.proxySetPreCheck(v, throw, s) { + p.proxySetPostCheck(target.self.getOwnPropSym(s), value, s) + return true + } + return false + } + return target.setSym(s, value, receiver, throw) +} + +func (p *proxyObject) setOwnStr(name unistring.String, v Value, throw bool) bool { + return p.proxySetStr(name, v, p.val, throw) +} + +func (p *proxyObject) setOwnIdx(idx valueInt, v Value, throw bool) bool { + return p.proxySetIdx(idx, v, p.val, throw) +} + +func (p *proxyObject) setOwnSym(s *Symbol, v Value, throw bool) bool { + return p.proxySetSym(s, v, p.val, throw) +} + +func (p *proxyObject) setForeignStr(name unistring.String, v, receiver Value, throw bool) (bool, bool) { + return p.proxySetStr(name, v, receiver, throw), true +} + +func (p *proxyObject) setForeignIdx(idx valueInt, v, receiver Value, throw bool) (bool, bool) { + return p.proxySetIdx(idx, v, receiver, throw), true +} + +func (p *proxyObject) setForeignSym(s *Symbol, v, receiver Value, throw bool) (bool, bool) { + return p.proxySetSym(s, v, receiver, throw), true +} + +func (p *proxyObject) proxyDeleteCheck(trapResult bool, targetProp Value, name fmt.Stringer, target *Object, throw bool) { + if trapResult { + if targetProp == nil { + return + } + if targetDesc, ok := targetProp.(*valueProperty); ok { + if !targetDesc.configurable { + panic(p.val.runtime.NewTypeError("'deleteProperty' on proxy: property '%s' is a non-configurable property but the trap returned truish", name.String())) + } + } + if !target.self.isExtensible() { + panic(p.val.runtime.NewTypeError("'deleteProperty' on proxy: trap returned truish for property '%s' but the proxy target is non-extensible", name.String())) + } + } else { + p.val.runtime.typeErrorResult(throw, "'deleteProperty' on proxy: trap returned falsish for property '%s'", name.String()) + } +} + +func (p *proxyObject) deleteStr(name unistring.String, throw bool) bool { + target := p.target + if v, ok := p.checkHandler().deleteStr(target, name); ok { + p.proxyDeleteCheck(v, target.self.getOwnPropStr(name), name, target, throw) + return v + } + + return target.self.deleteStr(name, throw) +} + +func (p *proxyObject) deleteIdx(idx valueInt, throw bool) bool { + target := p.target + if v, ok := p.checkHandler().deleteIdx(target, idx); ok { + p.proxyDeleteCheck(v, target.self.getOwnPropIdx(idx), idx, target, throw) + return v + } + + return target.self.deleteIdx(idx, throw) +} + +func (p *proxyObject) deleteSym(s *Symbol, throw bool) bool { + target := p.target + if v, ok := p.checkHandler().deleteSym(target, s); ok { + p.proxyDeleteCheck(v, target.self.getOwnPropSym(s), s, target, throw) + return v + } + + return target.self.deleteSym(s, throw) +} + +func (p *proxyObject) keys(all bool, _ []Value) []Value { + if v, ok := p.proxyOwnKeys(); ok { + if !all { + k := 0 + for i, key := range v { + prop := p.val.getOwnProp(key) + if prop == nil || prop == _undefined { + continue + } + if prop, ok := prop.(*valueProperty); ok && !prop.enumerable { + continue + } + if k != i { + v[k] = v[i] + } + k++ + } + v = v[:k] + } + return v + } + return p.target.self.keys(all, nil) +} + +func (p *proxyObject) proxyOwnKeys() ([]Value, bool) { + target := p.target + if v, ok := p.checkHandler().ownKeys(target); ok { + keys := p.val.runtime.toObject(v) + var keyList []Value + keySet := make(map[Value]struct{}) + l := toLength(keys.self.getStr("length", nil)) + for k := int64(0); k < l; k++ { + item := keys.self.getIdx(valueInt(k), nil) + if _, ok := item.(String); !ok { + if _, ok := item.(*Symbol); !ok { + panic(p.val.runtime.NewTypeError("%s is not a valid property name", item.String())) + } + } + if _, exists := keySet[item]; exists { + panic(p.val.runtime.NewTypeError("'ownKeys' on proxy: trap returned duplicate entries")) + } + keyList = append(keyList, item) + keySet[item] = struct{}{} + } + ext := target.self.isExtensible() + for item, next := target.self.iterateKeys()(); next != nil; item, next = next() { + if _, exists := keySet[item.name]; exists { + delete(keySet, item.name) + } else { + if !ext { + panic(p.val.runtime.NewTypeError("'ownKeys' on proxy: trap result did not include '%s'", item.name.String())) + } + var prop Value + if item.value == nil { + prop = target.getOwnProp(item.name) + } else { + prop = item.value + } + if prop, ok := prop.(*valueProperty); ok && !prop.configurable { + panic(p.val.runtime.NewTypeError("'ownKeys' on proxy: trap result did not include non-configurable '%s'", item.name.String())) + } + } + } + if !ext && len(keyList) > 0 && len(keySet) > 0 { + panic(p.val.runtime.NewTypeError("'ownKeys' on proxy: trap returned extra keys but proxy target is non-extensible")) + } + + return keyList, true + } + + return nil, false +} + +func (p *proxyObject) iterateStringKeys() iterNextFunc { + return (&proxyPropIter{ + p: p, + names: p.stringKeys(true, nil), + }).next +} + +func (p *proxyObject) iterateSymbols() iterNextFunc { + return (&proxyPropIter{ + p: p, + names: p.symbols(true, nil), + }).next +} + +func (p *proxyObject) iterateKeys() iterNextFunc { + return (&proxyPropIter{ + p: p, + names: p.keys(true, nil), + }).next +} + +func (p *proxyObject) assertCallable() (call func(FunctionCall) Value, ok bool) { + if p.call != nil { + return func(call FunctionCall) Value { + return p.apply(call) + }, true + } + return nil, false +} + +func (p *proxyObject) vmCall(vm *vm, n int) { + vm.pushCtx() + vm.prg = nil + vm.sb = vm.sp - n // so that [sb-1] points to the callee + ret := p.apply(FunctionCall{This: vm.stack[vm.sp-n-2], Arguments: vm.stack[vm.sp-n : vm.sp]}) + if ret == nil { + ret = _undefined + } + vm.stack[vm.sp-n-2] = ret + vm.popCtx() + vm.sp -= n + 1 + vm.pc++ +} + +func (p *proxyObject) assertConstructor() func(args []Value, newTarget *Object) *Object { + if p.ctor != nil { + return p.construct + } + return nil +} + +func (p *proxyObject) apply(call FunctionCall) Value { + if p.call == nil { + panic(p.val.runtime.NewTypeError("proxy target is not a function")) + } + if v, ok := p.checkHandler().apply(p.target, nilSafe(call.This), call.Arguments); ok { + return v + } + return p.call(call) +} + +func (p *proxyObject) construct(args []Value, newTarget *Object) *Object { + if p.ctor == nil { + panic(p.val.runtime.NewTypeError("proxy target is not a constructor")) + } + if newTarget == nil { + newTarget = p.val + } + if v, ok := p.checkHandler().construct(p.target, args, newTarget); ok { + return p.val.runtime.toObject(v) + } + return p.ctor(args, newTarget) +} + +func (p *proxyObject) __isCompatibleDescriptor(extensible bool, desc *PropertyDescriptor, current *valueProperty) bool { + if current == nil { + return extensible + } + + if !current.configurable { + if desc.Configurable == FLAG_TRUE { + return false + } + + if desc.Enumerable != FLAG_NOT_SET && desc.Enumerable.Bool() != current.enumerable { + return false + } + + if desc.IsGeneric() { + return true + } + + if desc.IsData() != !current.accessor { + return desc.Configurable != FLAG_FALSE + } + + if desc.IsData() && !current.accessor { + if !current.configurable { + if desc.Writable == FLAG_TRUE && !current.writable { + return false + } + if !current.writable { + if desc.Value != nil && !desc.Value.SameAs(current.value) { + return false + } + } + } + return true + } + if desc.IsAccessor() && current.accessor { + if !current.configurable { + if desc.Setter != nil && desc.Setter.SameAs(current.setterFunc) { + return false + } + if desc.Getter != nil && desc.Getter.SameAs(current.getterFunc) { + return false + } + } + } + } + return true +} + +func (p *proxyObject) __sameValue(val1, val2 Value) bool { + if val1 == nil && val2 == nil { + return true + } + if val1 != nil { + return val1.SameAs(val2) + } + return false +} + +func (p *proxyObject) filterKeys(vals []Value, all, symbols bool) []Value { + if !all { + k := 0 + for i, val := range vals { + var prop Value + if symbols { + if s, ok := val.(*Symbol); ok { + prop = p.getOwnPropSym(s) + } else { + continue + } + } else { + if _, ok := val.(*Symbol); !ok { + prop = p.getOwnPropStr(val.string()) + } else { + continue + } + } + if prop == nil { + continue + } + if prop, ok := prop.(*valueProperty); ok && !prop.enumerable { + continue + } + if k != i { + vals[k] = vals[i] + } + k++ + } + vals = vals[:k] + } else { + k := 0 + for i, val := range vals { + if _, ok := val.(*Symbol); ok != symbols { + continue + } + if k != i { + vals[k] = vals[i] + } + k++ + } + vals = vals[:k] + } + return vals +} + +func (p *proxyObject) stringKeys(all bool, _ []Value) []Value { // we can assume accum is empty + var keys []Value + if vals, ok := p.proxyOwnKeys(); ok { + keys = vals + } else { + keys = p.target.self.stringKeys(true, nil) + } + + return p.filterKeys(keys, all, false) +} + +func (p *proxyObject) symbols(all bool, accum []Value) []Value { + var symbols []Value + if vals, ok := p.proxyOwnKeys(); ok { + symbols = vals + } else { + symbols = p.target.self.symbols(true, nil) + } + symbols = p.filterKeys(symbols, all, true) + if accum == nil { + return symbols + } + accum = append(accum, symbols...) + return accum +} + +func (p *proxyObject) className() string { + if p.target == nil { + panic(p.val.runtime.NewTypeError("proxy has been revoked")) + } + if p.call != nil || p.ctor != nil { + return classFunction + } + return classObject +} + +func (p *proxyObject) typeOf() String { + if p.call == nil { + return stringObjectC + } + + return stringFunction +} + +func (p *proxyObject) exportType() reflect.Type { + return proxyType +} + +func (p *proxyObject) export(*objectExportCtx) interface{} { + return Proxy{ + proxy: p, + } +} + +func (p *proxyObject) revoke() { + p.handler = nil + p.target = nil +} diff --git a/goja/regexp.go b/goja/regexp.go new file mode 100644 index 0000000..f70c34d --- /dev/null +++ b/goja/regexp.go @@ -0,0 +1,650 @@ +package goja + +import ( + "fmt" + "github.com/dlclark/regexp2" + "github.com/dop251/goja/unistring" + "io" + "regexp" + "sort" + "strings" + "unicode/utf16" +) + +type regexp2MatchCache struct { + target String + runes []rune + posMap []int +} + +// Not goroutine-safe. Use regexp2Wrapper.clone() +type regexp2Wrapper struct { + rx *regexp2.Regexp + cache *regexp2MatchCache +} + +type regexpWrapper regexp.Regexp + +type positionMapItem struct { + src, dst int +} +type positionMap []positionMapItem + +func (m positionMap) get(src int) int { + if src <= 0 { + return src + } + res := sort.Search(len(m), func(n int) bool { return m[n].src >= src }) + if res >= len(m) || m[res].src != src { + panic("index not found") + } + return m[res].dst +} + +type arrayRuneReader struct { + runes []rune + pos int +} + +func (rd *arrayRuneReader) ReadRune() (r rune, size int, err error) { + if rd.pos < len(rd.runes) { + r = rd.runes[rd.pos] + size = 1 + rd.pos++ + } else { + err = io.EOF + } + return +} + +// Not goroutine-safe. Use regexpPattern.clone() +type regexpPattern struct { + src string + + global, ignoreCase, multiline, dotAll, sticky, unicode bool + + regexpWrapper *regexpWrapper + regexp2Wrapper *regexp2Wrapper +} + +func compileRegexp2(src string, multiline, dotAll, ignoreCase, unicode bool) (*regexp2Wrapper, error) { + var opts regexp2.RegexOptions = regexp2.ECMAScript + if multiline { + opts |= regexp2.Multiline + } + if dotAll { + opts |= regexp2.Singleline + } + if ignoreCase { + opts |= regexp2.IgnoreCase + } + if unicode { + opts |= regexp2.Unicode + } + regexp2Pattern, err1 := regexp2.Compile(src, opts) + if err1 != nil { + return nil, fmt.Errorf("Invalid regular expression (regexp2): %s (%v)", src, err1) + } + + return ®exp2Wrapper{rx: regexp2Pattern}, nil +} + +func (p *regexpPattern) createRegexp2() { + if p.regexp2Wrapper != nil { + return + } + rx, err := compileRegexp2(p.src, p.multiline, p.dotAll, p.ignoreCase, p.unicode) + if err != nil { + // At this point the regexp should have been successfully converted to re2, if it fails now, it's a bug. + panic(err) + } + p.regexp2Wrapper = rx +} + +func buildUTF8PosMap(s unicodeString) (positionMap, string) { + pm := make(positionMap, 0, s.Length()) + rd := s.Reader() + sPos, utf8Pos := 0, 0 + var sb strings.Builder + for { + r, size, err := rd.ReadRune() + if err == io.EOF { + break + } + if err != nil { + // the string contains invalid UTF-16, bailing out + return nil, "" + } + utf8Size, _ := sb.WriteRune(r) + sPos += size + utf8Pos += utf8Size + pm = append(pm, positionMapItem{src: utf8Pos, dst: sPos}) + } + return pm, sb.String() +} + +func (p *regexpPattern) findSubmatchIndex(s String, start int) []int { + if p.regexpWrapper == nil { + return p.regexp2Wrapper.findSubmatchIndex(s, start, p.unicode, p.global || p.sticky) + } + if start != 0 { + // Unfortunately Go's regexp library does not allow starting from an arbitrary position. + // If we just drop the first _start_ characters of the string the assertions (^, $, \b and \B) will not + // work correctly. + p.createRegexp2() + return p.regexp2Wrapper.findSubmatchIndex(s, start, p.unicode, p.global || p.sticky) + } + return p.regexpWrapper.findSubmatchIndex(s, p.unicode) +} + +func (p *regexpPattern) findAllSubmatchIndex(s String, start int, limit int, sticky bool) [][]int { + if p.regexpWrapper == nil { + return p.regexp2Wrapper.findAllSubmatchIndex(s, start, limit, sticky, p.unicode) + } + if start == 0 { + a, u := devirtualizeString(s) + if u == nil { + return p.regexpWrapper.findAllSubmatchIndex(string(a), limit, sticky) + } + if limit == 1 { + result := p.regexpWrapper.findSubmatchIndexUnicode(u, p.unicode) + if result == nil { + return nil + } + return [][]int{result} + } + // Unfortunately Go's regexp library lacks FindAllReaderSubmatchIndex(), so we have to use a UTF-8 string as an + // input. + if p.unicode { + // Try to convert s to UTF-8. If it does not contain any invalid UTF-16 we can do the matching in UTF-8. + pm, str := buildUTF8PosMap(u) + if pm != nil { + res := p.regexpWrapper.findAllSubmatchIndex(str, limit, sticky) + for _, result := range res { + for i, idx := range result { + result[i] = pm.get(idx) + } + } + return res + } + } + } + + p.createRegexp2() + return p.regexp2Wrapper.findAllSubmatchIndex(s, start, limit, sticky, p.unicode) +} + +// clone creates a copy of the regexpPattern which can be used concurrently. +func (p *regexpPattern) clone() *regexpPattern { + ret := ®expPattern{ + src: p.src, + global: p.global, + ignoreCase: p.ignoreCase, + multiline: p.multiline, + dotAll: p.dotAll, + sticky: p.sticky, + unicode: p.unicode, + } + if p.regexpWrapper != nil { + ret.regexpWrapper = p.regexpWrapper.clone() + } + if p.regexp2Wrapper != nil { + ret.regexp2Wrapper = p.regexp2Wrapper.clone() + } + return ret +} + +type regexpObject struct { + baseObject + pattern *regexpPattern + source String + + standard bool +} + +func (r *regexp2Wrapper) findSubmatchIndex(s String, start int, fullUnicode, doCache bool) (result []int) { + if fullUnicode { + return r.findSubmatchIndexUnicode(s, start, doCache) + } + return r.findSubmatchIndexUTF16(s, start, doCache) +} + +func (r *regexp2Wrapper) findUTF16Cached(s String, start int, doCache bool) (match *regexp2.Match, runes []rune, err error) { + wrapped := r.rx + cache := r.cache + if cache != nil && cache.posMap == nil && cache.target.SameAs(s) { + runes = cache.runes + } else { + runes = s.utf16Runes() + cache = nil + } + match, err = wrapped.FindRunesMatchStartingAt(runes, start) + if doCache && match != nil && err == nil { + if cache == nil { + if r.cache == nil { + r.cache = new(regexp2MatchCache) + } + *r.cache = regexp2MatchCache{ + target: s, + runes: runes, + } + } + } else { + r.cache = nil + } + return +} + +func (r *regexp2Wrapper) findSubmatchIndexUTF16(s String, start int, doCache bool) (result []int) { + match, _, err := r.findUTF16Cached(s, start, doCache) + if err != nil { + return + } + + if match == nil { + return + } + groups := match.Groups() + + result = make([]int, 0, len(groups)<<1) + for _, group := range groups { + if len(group.Captures) > 0 { + result = append(result, group.Index, group.Index+group.Length) + } else { + result = append(result, -1, 0) + } + } + return +} + +func (r *regexp2Wrapper) findUnicodeCached(s String, start int, doCache bool) (match *regexp2.Match, posMap []int, err error) { + var ( + runes []rune + mappedStart int + splitPair bool + savedRune rune + ) + wrapped := r.rx + cache := r.cache + if cache != nil && cache.posMap != nil && cache.target.SameAs(s) { + runes, posMap = cache.runes, cache.posMap + mappedStart, splitPair = posMapReverseLookup(posMap, start) + } else { + posMap, runes, mappedStart, splitPair = buildPosMap(&lenientUtf16Decoder{utf16Reader: s.utf16Reader()}, s.Length(), start) + cache = nil + } + if splitPair { + // temporarily set the rune at mappedStart to the second code point of the pair + _, second := utf16.EncodeRune(runes[mappedStart]) + savedRune, runes[mappedStart] = runes[mappedStart], second + } + match, err = wrapped.FindRunesMatchStartingAt(runes, mappedStart) + if doCache && match != nil && err == nil { + if splitPair { + runes[mappedStart] = savedRune + } + if cache == nil { + if r.cache == nil { + r.cache = new(regexp2MatchCache) + } + *r.cache = regexp2MatchCache{ + target: s, + runes: runes, + posMap: posMap, + } + } + } else { + r.cache = nil + } + + return +} + +func (r *regexp2Wrapper) findSubmatchIndexUnicode(s String, start int, doCache bool) (result []int) { + match, posMap, err := r.findUnicodeCached(s, start, doCache) + if match == nil || err != nil { + return + } + + groups := match.Groups() + + result = make([]int, 0, len(groups)<<1) + for _, group := range groups { + if len(group.Captures) > 0 { + result = append(result, posMap[group.Index], posMap[group.Index+group.Length]) + } else { + result = append(result, -1, 0) + } + } + return +} + +func (r *regexp2Wrapper) findAllSubmatchIndexUTF16(s String, start, limit int, sticky bool) [][]int { + wrapped := r.rx + match, runes, err := r.findUTF16Cached(s, start, false) + if match == nil || err != nil { + return nil + } + if limit < 0 { + limit = len(runes) + 1 + } + results := make([][]int, 0, limit) + for match != nil { + groups := match.Groups() + + result := make([]int, 0, len(groups)<<1) + + for _, group := range groups { + if len(group.Captures) > 0 { + startPos := group.Index + endPos := group.Index + group.Length + result = append(result, startPos, endPos) + } else { + result = append(result, -1, 0) + } + } + + if sticky && len(result) > 1 { + if result[0] != start { + break + } + start = result[1] + } + + results = append(results, result) + limit-- + if limit <= 0 { + break + } + match, err = wrapped.FindNextMatch(match) + if err != nil { + return nil + } + } + return results +} + +func buildPosMap(rd io.RuneReader, l, start int) (posMap []int, runes []rune, mappedStart int, splitPair bool) { + posMap = make([]int, 0, l+1) + curPos := 0 + runes = make([]rune, 0, l) + startFound := false + for { + if !startFound { + if curPos == start { + mappedStart = len(runes) + startFound = true + } + if curPos > start { + // start position splits a surrogate pair + mappedStart = len(runes) - 1 + splitPair = true + startFound = true + } + } + rn, size, err := rd.ReadRune() + if err != nil { + break + } + runes = append(runes, rn) + posMap = append(posMap, curPos) + curPos += size + } + posMap = append(posMap, curPos) + return +} + +func posMapReverseLookup(posMap []int, pos int) (int, bool) { + mapped := sort.SearchInts(posMap, pos) + if mapped < len(posMap) && posMap[mapped] != pos { + return mapped - 1, true + } + return mapped, false +} + +func (r *regexp2Wrapper) findAllSubmatchIndexUnicode(s unicodeString, start, limit int, sticky bool) [][]int { + wrapped := r.rx + if limit < 0 { + limit = len(s) + 1 + } + results := make([][]int, 0, limit) + match, posMap, err := r.findUnicodeCached(s, start, false) + if err != nil { + return nil + } + for match != nil { + groups := match.Groups() + + result := make([]int, 0, len(groups)<<1) + + for _, group := range groups { + if len(group.Captures) > 0 { + start := posMap[group.Index] + end := posMap[group.Index+group.Length] + result = append(result, start, end) + } else { + result = append(result, -1, 0) + } + } + + if sticky && len(result) > 1 { + if result[0] != start { + break + } + start = result[1] + } + + results = append(results, result) + match, err = wrapped.FindNextMatch(match) + if err != nil { + return nil + } + } + return results +} + +func (r *regexp2Wrapper) findAllSubmatchIndex(s String, start, limit int, sticky, fullUnicode bool) [][]int { + a, u := devirtualizeString(s) + if u != nil { + if fullUnicode { + return r.findAllSubmatchIndexUnicode(u, start, limit, sticky) + } + return r.findAllSubmatchIndexUTF16(u, start, limit, sticky) + } + return r.findAllSubmatchIndexUTF16(a, start, limit, sticky) +} + +func (r *regexp2Wrapper) clone() *regexp2Wrapper { + return ®exp2Wrapper{ + rx: r.rx, + } +} + +func (r *regexpWrapper) findAllSubmatchIndex(s string, limit int, sticky bool) (results [][]int) { + wrapped := (*regexp.Regexp)(r) + results = wrapped.FindAllStringSubmatchIndex(s, limit) + pos := 0 + if sticky { + for i, result := range results { + if len(result) > 1 { + if result[0] != pos { + return results[:i] + } + pos = result[1] + } + } + } + return +} + +func (r *regexpWrapper) findSubmatchIndex(s String, fullUnicode bool) []int { + a, u := devirtualizeString(s) + if u != nil { + return r.findSubmatchIndexUnicode(u, fullUnicode) + } + return r.findSubmatchIndexASCII(string(a)) +} + +func (r *regexpWrapper) findSubmatchIndexASCII(s string) []int { + wrapped := (*regexp.Regexp)(r) + return wrapped.FindStringSubmatchIndex(s) +} + +func (r *regexpWrapper) findSubmatchIndexUnicode(s unicodeString, fullUnicode bool) (result []int) { + wrapped := (*regexp.Regexp)(r) + if fullUnicode { + posMap, runes, _, _ := buildPosMap(&lenientUtf16Decoder{utf16Reader: s.utf16Reader()}, s.Length(), 0) + res := wrapped.FindReaderSubmatchIndex(&arrayRuneReader{runes: runes}) + for i, item := range res { + if item >= 0 { + res[i] = posMap[item] + } + } + return res + } + return wrapped.FindReaderSubmatchIndex(s.utf16RuneReader()) +} + +func (r *regexpWrapper) clone() *regexpWrapper { + return r +} + +func (r *regexpObject) execResultToArray(target String, result []int) Value { + captureCount := len(result) >> 1 + valueArray := make([]Value, captureCount) + matchIndex := result[0] + valueArray[0] = target.Substring(result[0], result[1]) + lowerBound := 0 + for index := 1; index < captureCount; index++ { + offset := index << 1 + if result[offset] >= 0 && result[offset+1] >= lowerBound { + valueArray[index] = target.Substring(result[offset], result[offset+1]) + lowerBound = result[offset] + } else { + valueArray[index] = _undefined + } + } + match := r.val.runtime.newArrayValues(valueArray) + match.self.setOwnStr("input", target, false) + match.self.setOwnStr("index", intToValue(int64(matchIndex)), false) + return match +} + +func (r *regexpObject) getLastIndex() int64 { + lastIndex := toLength(r.getStr("lastIndex", nil)) + if !r.pattern.global && !r.pattern.sticky { + return 0 + } + return lastIndex +} + +func (r *regexpObject) updateLastIndex(index int64, firstResult, lastResult []int) bool { + if r.pattern.sticky { + if firstResult == nil || int64(firstResult[0]) != index { + r.setOwnStr("lastIndex", intToValue(0), true) + return false + } + } else { + if firstResult == nil { + if r.pattern.global { + r.setOwnStr("lastIndex", intToValue(0), true) + } + return false + } + } + + if r.pattern.global || r.pattern.sticky { + r.setOwnStr("lastIndex", intToValue(int64(lastResult[1])), true) + } + return true +} + +func (r *regexpObject) execRegexp(target String) (match bool, result []int) { + index := r.getLastIndex() + if index >= 0 && index <= int64(target.Length()) { + result = r.pattern.findSubmatchIndex(target, int(index)) + } + match = r.updateLastIndex(index, result, result) + return +} + +func (r *regexpObject) exec(target String) Value { + match, result := r.execRegexp(target) + if match { + return r.execResultToArray(target, result) + } + return _null +} + +func (r *regexpObject) test(target String) bool { + match, _ := r.execRegexp(target) + return match +} + +func (r *regexpObject) clone() *regexpObject { + r1 := r.val.runtime.newRegexpObject(r.prototype) + r1.source = r.source + r1.pattern = r.pattern + + return r1 +} + +func (r *regexpObject) init() { + r.baseObject.init() + r.standard = true + r._putProp("lastIndex", intToValue(0), true, false, false) +} + +func (r *regexpObject) setProto(proto *Object, throw bool) bool { + res := r.baseObject.setProto(proto, throw) + if res { + r.standard = false + } + return res +} + +func (r *regexpObject) defineOwnPropertyStr(name unistring.String, desc PropertyDescriptor, throw bool) bool { + res := r.baseObject.defineOwnPropertyStr(name, desc, throw) + if res { + r.standard = false + } + return res +} + +func (r *regexpObject) defineOwnPropertySym(name *Symbol, desc PropertyDescriptor, throw bool) bool { + res := r.baseObject.defineOwnPropertySym(name, desc, throw) + if res && r.standard { + switch name { + case SymMatch, SymMatchAll, SymSearch, SymSplit, SymReplace: + r.standard = false + } + } + return res +} + +func (r *regexpObject) deleteStr(name unistring.String, throw bool) bool { + res := r.baseObject.deleteStr(name, throw) + if res { + r.standard = false + } + return res +} + +func (r *regexpObject) setOwnStr(name unistring.String, value Value, throw bool) bool { + res := r.baseObject.setOwnStr(name, value, throw) + if res && r.standard && name == "exec" { + r.standard = false + } + return res +} + +func (r *regexpObject) setOwnSym(name *Symbol, value Value, throw bool) bool { + res := r.baseObject.setOwnSym(name, value, throw) + if res && r.standard { + switch name { + case SymMatch, SymMatchAll, SymSearch, SymSplit, SymReplace: + r.standard = false + } + } + return res +} diff --git a/goja/regexp_test.go b/goja/regexp_test.go new file mode 100644 index 0000000..04498e5 --- /dev/null +++ b/goja/regexp_test.go @@ -0,0 +1,960 @@ +package goja + +import ( + "testing" +) + +func TestRegexp1(t *testing.T) { + const SCRIPT = ` + var r = new RegExp("(['\"])(.*?)\\1"); + var m = r.exec("'test'"); + m !== null && m.length == 3 && m[2] === "test"; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestRegexp2(t *testing.T) { + const SCRIPT = ` + var r = new RegExp("(['\"])(.*?)['\"]"); + var m = r.exec("'test'"); + m !== null && m.length == 3 && m[2] === "test"; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestRegexpLiteral(t *testing.T) { + const SCRIPT = ` + var r = /(['\"])(.*?)\1/; + var m = r.exec("'test'"); + m !== null && m.length == 3 && m[2] === "test"; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestRegexpRe2Unicode(t *testing.T) { + const SCRIPT = ` + var r = /(тест)/i; + var m = r.exec("'Тест'"); + m !== null && m.length == 2 && m[1] === "Тест"; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestRegexpRe2UnicodeTarget(t *testing.T) { + const SCRIPT = ` + var r = /(['\"])(.*?)['\"]/i; + var m = r.exec("'Тест'"); + m !== null && m.length == 3 && m[2] === "Тест"; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestRegexpRegexp2Unicode(t *testing.T) { + const SCRIPT = ` + var r = /(['\"])(тест)\1/i; + var m = r.exec("'Тест'"); + m !== null && m.length == 3 && m[2] === "Тест"; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestRegexpRegexp2UnicodeTarget(t *testing.T) { + const SCRIPT = ` + var r = /(['\"])(.*?)\1/; + var m = r.exec("'Тест'"); + m !== null && m.length == 3 && m[2] === "Тест"; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestRegexpRe2Whitespace(t *testing.T) { + const SCRIPT = ` + "\u2000\u2001\u2002\u200b".replace(/\s+/g, "") === "\u200b"; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestRegexpRegexp2Whitespace(t *testing.T) { + const SCRIPT = ` + "A\u2000\u2001\u2002A\u200b".replace(/(A)\s+\1/g, "") === "\u200b" + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestEmptyCharClassRe2(t *testing.T) { + const SCRIPT = ` + /[]/.test("\u0000"); + ` + + testScript(SCRIPT, valueFalse, t) +} + +func TestNegatedEmptyCharClassRe2(t *testing.T) { + const SCRIPT = ` + /[^]/.test("\u0000"); + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestEmptyCharClassRegexp2(t *testing.T) { + const SCRIPT = ` + /([])\1/.test("\u0000\u0000"); + ` + + testScript(SCRIPT, valueFalse, t) +} + +func TestRegexp2Negate(t *testing.T) { + const SCRIPT = ` + /([\D1])\1/.test("aa"); + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestAlternativeRe2(t *testing.T) { + const SCRIPT = ` + /()|/.exec("") !== null; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestRegexpReplaceGlobal(t *testing.T) { + const SCRIPT = ` + "QBZPbage\ny_cynprubyqre".replace(/^\s*|\s*$/g, '') + ` + + testScript(SCRIPT, asciiString("QBZPbage\ny_cynprubyqre"), t) +} + +func TestRegexpNumCaptures(t *testing.T) { + const SCRIPT = ` + "Fubpxjnir Synfu 9.0 e115".replace(/([a-zA-Z]|\s)+/, '') + ` + testScript(SCRIPT, asciiString("9.0 e115"), t) +} + +func TestRegexpNumCaptures1(t *testing.T) { + const SCRIPT = ` + "Fubpxjnir Sy\tfu 9.0 e115".replace(/^.*\s+(\S+\s+\S+$)/, '') + ` + testScript(SCRIPT, asciiString(""), t) +} + +func TestRegexpSInClass(t *testing.T) { + const SCRIPT = ` + /[\S]/.test("\u2028"); + ` + testScript(SCRIPT, valueFalse, t) +} + +func TestRegexpDotMatchCR(t *testing.T) { + const SCRIPT = ` + /./.test("\r"); + ` + + testScript(SCRIPT, valueFalse, t) +} + +func TestRegexpDotMatchCRInGroup(t *testing.T) { + const SCRIPT = ` + /(.)/.test("\r"); + ` + + testScript(SCRIPT, valueFalse, t) +} + +func TestRegexpDotMatchLF(t *testing.T) { + const SCRIPT = ` + /./.test("\n"); + ` + + testScript(SCRIPT, valueFalse, t) +} + +func TestRegexpSplitWithBackRef(t *testing.T) { + const SCRIPT = ` + "a++b+-c".split(/([+-])\1/).join(" $$ ") + ` + + testScript(SCRIPT, asciiString("a $$ + $$ b+-c"), t) +} + +func TestEscapeNonASCII(t *testing.T) { + const SCRIPT = ` + /\⩓/.test("⩓") + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestRegexpUTF16(t *testing.T) { + const SCRIPT = ` + var str = "\uD800\uDC00"; + + assert(/\uD800/g.test(str), "#1"); + assert(/\uD800/.test(str), "#2"); + assert(/𐀀/.test(str), "#3"); + + var re = /\uD800/; + + assert(compareArray(str.replace(re, "X"), ["X", "\uDC00"]), "#4"); + assert(compareArray(str.split(re), ["", "\uDC00"]), "#5"); + assert(compareArray("a\uD800\uDC00b".split(/\uD800/g), ["a", "\uDC00b"]), "#6"); + assert(compareArray("a\uD800\uDC00b".split(/(?:)/g), ["a", "\uD800", "\uDC00", "b"]), "#7"); + assert(compareArray("0\x80".split(/(0){0}/g), ["0", undefined, "\x80"]), "#7+"); + + re = /(?=)a/; // a hack to use regexp2 + assert.sameValue(re.exec('\ud83d\ude02a').index, 2, "#8"); + + assert.sameValue(/./.exec('\ud83d\ude02')[0], '\ud83d', "#9"); + + assert(RegExp("\uD800").test("\uD800"), "#10"); + + var cu = 0xD800; + var xx = "a\\" + String.fromCharCode(cu); + var pattern = eval("/" + xx + "/"); + assert.sameValue(pattern.source, "a\\\\\\ud800", "Code unit: " + cu.toString(16), "#11"); + assert(pattern.test("a\\\uD800"), "#12"); + ` + + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestRegexpUnicode(t *testing.T) { + const SCRIPT = ` + + assert(!/\uD800/u.test("\uD800\uDC00"), "#1"); + assert(!/\uFFFD/u.test("\uD800\uDC00"), "#2"); + + assert(/\uD800\uDC00/u.test("\uD800\uDC00"), "#3"); + + assert(/\uD800/u.test("\uD800"), "#4"); + + assert(compareArray("a\uD800\uDC00b".split(/\uD800/gu), ["a\uD800\uDC00b"]), "#5"); + + assert(compareArray("a\uD800\uDC00b".split(/(?:)/gu), ["a", "𐀀", "b"]), "#6"); + + assert(compareArray("0\x80".split(/(0){0}/gu), ["0", undefined, "\x80"]), "#7"); + + var re = eval('/' + /\ud834\udf06/u.source + '/u'); + assert(re.test('\ud834\udf06'), "#9"); + + /*re = RegExp("\\p{L}", "u"); + if (!re.test("A")) { + throw new Error("Test 9 failed"); + }*/ + ` + + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestConvertRegexpToUnicode(t *testing.T) { + if s := convertRegexpToUnicode(`test\uD800\u0C00passed`); s != `test\uD800\u0C00passed` { + t.Fatal(s) + } + if s := convertRegexpToUnicode(`test\uD800\uDC00passed`); s != `test𐀀passed` { + t.Fatal(s) + } + if s := convertRegexpToUnicode(`test\u0023passed`); s != `test\u0023passed` { + t.Fatal(s) + } + if s := convertRegexpToUnicode(`test\u0passed`); s != `test\u0passed` { + t.Fatal(s) + } + if s := convertRegexpToUnicode(`test\uD800passed`); s != `test\uD800passed` { + t.Fatal(s) + } + if s := convertRegexpToUnicode(`test\uD800`); s != `test\uD800` { + t.Fatal(s) + } + if s := convertRegexpToUnicode(`test\uD80`); s != `test\uD80` { + t.Fatal(s) + } + if s := convertRegexpToUnicode(`\\uD800\uDC00passed`); s != `\\uD800\uDC00passed` { + t.Fatal(s) + } + if s := convertRegexpToUnicode(`testpassed`); s != `testpassed` { + t.Fatal(s) + } +} + +func TestConvertRegexpToUtf16(t *testing.T) { + if s := convertRegexpToUtf16(`𐀀`); s != `\ud800\udc00` { + t.Fatal(s) + } + if s := convertRegexpToUtf16(`\𐀀`); s != `\\\ud800\udc00` { + t.Fatal(s) + } +} + +func TestEscapeInvalidUtf16(t *testing.T) { + if s := escapeInvalidUtf16(asciiString("test")); s != "test" { + t.Fatal(s) + } + if s := escapeInvalidUtf16(newStringValue("test\U00010000")); s != "test\U00010000" { + t.Fatal(s) + } + if s := escapeInvalidUtf16(unicodeStringFromRunes([]rune{'t', 0xD800})); s != "t\\ud800" { + t.Fatal(s) + } + if s := escapeInvalidUtf16(unicodeStringFromRunes([]rune{'t', 0xD800, 'p'})); s != "t\\ud800p" { + t.Fatal(s) + } + if s := escapeInvalidUtf16(unicodeStringFromRunes([]rune{0xD800, 'p'})); s != "\\ud800p" { + t.Fatal(s) + } + if s := escapeInvalidUtf16(unicodeStringFromRunes([]rune{'t', '\\', 0xD800, 'p'})); s != `t\\\ud800p` { + t.Fatal(s) + } +} + +func TestRegexpAssertion(t *testing.T) { + const SCRIPT = ` + var res = 'aaa'.match(/^a/g); + res.length === 1 || res[0] === 'a'; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestRegexpUnicodeAdvanceStringIndex(t *testing.T) { + const SCRIPT = ` + // deoptimise RegExp + var origExec = RegExp.prototype.exec; + RegExp.prototype.exec = function(s) { + return origExec.call(this, s); + }; + + var re = /(?:)/gu; + var str = "a\uD800\uDC00b"; + assert(compareArray(str.split(re), ["a", "𐀀", "b"]), "#1"); + + re.lastIndex = 3; + assert.sameValue(re.exec(str).index, 3, "#2"); + + re.lastIndex = 2; + assert.sameValue(re.exec(str).index, 1, "#3"); + + re.lastIndex = 4; + assert.sameValue(re.exec(str).index, 4, "#4"); + + re.lastIndex = 5; + assert.sameValue(re.exec(str), null, "#5"); + + var iterator = str.matchAll(re); // regexp is copied by matchAll, but resets lastIndex + var matches = []; + for (var v of iterator) {matches.push(v);} + assert.sameValue(matches.length, 4, "#6"); + assert.sameValue(matches[0].index, 0, "#7 index"); + assert.sameValue(matches[0][0], "", "#7 value"); + assert.sameValue(matches[1].index, 1, "#8 index"); + assert.sameValue(matches[1][0], "", "#8 value"); + assert.sameValue(matches[2].index, 3, "#9 index"); + assert.sameValue(matches[2][0], "", "#9 value"); + assert.sameValue(matches[3].index, 4, "#10 index"); + assert.sameValue(matches[3][0], "", "#10 value"); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestRegexpInit(t *testing.T) { + const SCRIPT = ` + RegExp(".").lastIndex; + ` + testScript(SCRIPT, intToValue(0), t) +} + +func TestRegexpToString(t *testing.T) { + const SCRIPT = ` + RegExp.prototype.toString.call({ + source: 'foo', + flags: 'bar'}); + ` + testScript(SCRIPT, asciiString("/foo/bar"), t) +} + +func TestRegexpEscapeSource(t *testing.T) { + const SCRIPT = ` + /href="(.+?)(\/.*\/\S+?)\/"/.source; + ` + testScript(SCRIPT, asciiString(`href="(.+?)(\/.*\/\S+?)\/"`), t) +} + +func TestRegexpConsecutiveMatchCache(t *testing.T) { + const SCRIPT = ` + (function test(unicode) { + var regex = new RegExp('t(e)(st(\\d?))', unicode?'gu':'g'); + var string = 'test1test2'; + var match; + var matches = []; + while (match = regex.exec(string)) { + matches.push(match); + } + var expectedMatches = [ + [ + 'test1', + 'e', + 'st1', + '1' + ], + [ + 'test2', + 'e', + 'st2', + '2' + ] + ]; + expectedMatches[0].index = 0; + expectedMatches[0].input = 'test1test2'; + expectedMatches[1].index = 5; + expectedMatches[1].input = 'test1test2'; + + assert(deepEqual(matches, expectedMatches), "#1"); + + // try the same regexp with a different string + regex.lastIndex = 0; + match = regex.exec(' test5'); + var expectedMatch = [ + 'test5', + 'e', + 'st5', + '5' + ]; + expectedMatch.index = 1; + expectedMatch.input = ' test5'; + assert(deepEqual(match, expectedMatch), "#2"); + assert.sameValue(regex.lastIndex, 6, "#3"); + + // continue matching with a different string + match = regex.exec(' test5test6'); + expectedMatch = [ + 'test6', + 'e', + 'st6', + '6' + ]; + expectedMatch.index = 6; + expectedMatch.input = ' test5test6'; + assert(deepEqual(match, expectedMatch), "#4"); + assert.sameValue(regex.lastIndex, 11, "#5"); + + match = regex.exec(' test5test6'); + assert.sameValue(match, null, "#6"); + return regex; + }); + ` + vm := New() + _, _ = vm.RunProgram(testLib()) + _, _ = vm.RunProgram(testLibX()) + v, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + var f func(bool) (*Object, error) + err = vm.ExportTo(v, &f) + if err != nil { + t.Fatal(err) + } + + regex, err := f(false) + if err != nil { + t.Fatal(err) + } + if regex.self.(*regexpObject).pattern.regexp2Wrapper.cache != nil { + t.Fatal("Cache is not nil (non-unicode)") + } + + regex, err = f(true) + if err != nil { + t.Fatal(err) + } + if regex.self.(*regexpObject).pattern.regexp2Wrapper.cache != nil { + t.Fatal("Cache is not nil (unicode)") + } +} + +func TestRegexpMatchAll(t *testing.T) { + const SCRIPT = ` + (function test(unicode) { + var regex = new RegExp('t(e)(st(\\d?))', unicode?'gu':'g'); + var string = 'test1test2'; + var matches = []; + for (var match of string.matchAll(regex)) { + matches.push(match); + } + var expectedMatches = [ + [ + 'test1', + 'e', + 'st1', + '1' + ], + [ + 'test2', + 'e', + 'st2', + '2' + ] + ]; + expectedMatches[0].index = 0; + expectedMatches[0].input = 'test1test2'; + expectedMatches[1].index = 5; + expectedMatches[1].input = 'test1test2'; + + assert(deepEqual(matches, expectedMatches), "#1"); + assert.sameValue(regex.lastIndex, 0, "#1 lastIndex"); + + // try the same regexp with a different string + string = ' test5'; + matches = []; + for (var match of string.matchAll(regex)) { + matches.push(match); + } + expectedMatches = [ + [ + 'test5', + 'e', + 'st5', + '5' + ] + ]; + expectedMatches[0].index = 1; + expectedMatches[0].input = ' test5'; + assert(deepEqual(matches, expectedMatches), "#2"); + assert.sameValue(regex.lastIndex, 0, "#2 lastIndex"); + + // continue matching with a different string + string = ' test5test6'; + matches = []; + for (var match of string.matchAll(regex)) { + matches.push(match); + } + var expectedMatches = [ + [ + 'test5', + 'e', + 'st5', + '5' + ], + [ + 'test6', + 'e', + 'st6', + '6' + ] + ]; + expectedMatches[0].index = 1; + expectedMatches[0].input = ' test5test6'; + expectedMatches[1].index = 6; + expectedMatches[1].input = ' test5test6'; + assert(deepEqual(matches, expectedMatches), "#3"); + assert.sameValue(regex.lastIndex, 0, "#3 lastindex"); + }); + ` + vm := New() + _, _ = vm.RunProgram(testLib()) + _, _ = vm.RunProgram(testLibX()) + v, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + var f func(bool) (*Object, error) + err = vm.ExportTo(v, &f) + if err != nil { + t.Fatal(err) + } + + _, err = f(false) + if err != nil { + t.Fatal(err) + } + + _, err = f(true) + if err != nil { + t.Fatal(err) + } +} + +func TestRegexpOverrideSpecies(t *testing.T) { + const SCRIPT = ` + Object.defineProperty(RegExp, Symbol.species, { + configurable: true, + value: function() { + throw "passed"; + } + }); + try { + "ab".split(/a/); + throw new Error("Expected error"); + } catch(e) { + if (e !== "passed") { + throw e; + } + } + ` + testScript(SCRIPT, _undefined, t) +} + +func TestRegexpSymbolMatchAllCallsIsRegexp(t *testing.T) { + // This is tc39's test/built-ins/RegExp/prototype/Symbol.matchAll/isregexp-this-throws.js + const SCRIPT = ` + var a = new Object(); + Object.defineProperty(a, Symbol.match, { + get: function() { + throw "passed"; + } + }); + try { + RegExp.prototype[Symbol.matchAll].call(a, ''); + throw new Error("Expected error"); + } catch(e) { + if (e !== "passed") { + throw e; + } + } + ` + testScript(SCRIPT, _undefined, t) +} + +func TestRegexpMatchAllConstructor(t *testing.T) { + // This is tc39's test/built-ins/RegExp/prototype/Symbol.matchAll/species-constuctor.js + const SCRIPT = ` + var callCount = 0; + var callArgs; + var regexp = /\d/u; + var obj = {} + Object.defineProperty(obj, Symbol.species, { + value: function() { + callCount++; + callArgs = arguments; + return /\w/g; + } + }); + regexp.constructor = obj; + var str = 'a*b'; + var iter = regexp[Symbol.matchAll](str); + + assert.sameValue(callCount, 1); + assert.sameValue(callArgs.length, 2); + assert.sameValue(callArgs[0], regexp); + assert.sameValue(callArgs[1], 'u'); + + var first = iter.next() + assert.sameValue(first.done, false); + assert.sameValue(first.value.length, 1); + assert.sameValue(first.value[0], "a"); + var second = iter.next() + assert.sameValue(second.done, true); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestRegexp2InvalidEscape(t *testing.T) { + testScript(`/(?=)\x0/.test("x0")`, valueTrue, t) +} + +func TestRegexpUnicodeEmptyMatch(t *testing.T) { + testScript(`/(0)0|/gu.exec("0\xef").length === 2`, valueTrue, t) +} + +func TestRegexpInvalidGroup(t *testing.T) { + const SCRIPT = ` + ["?", "(?)"].forEach(function(s) { + assert.throws(SyntaxError, function() {new RegExp(s)}, s); + }); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestRegexpLookbehindAssertion(t *testing.T) { + const SCRIPT = ` + var re = /(?<=Jack|Tom)Sprat/; + assert(re.test("JackSprat"), "#1"); + assert(!re.test("JohnSprat"), "#2"); + + re = /(? { + new RegExp("(?<=a)\\u{6_5}", "u"); + }); + + assert.throws(SyntaxError, () => { + new RegExp("a\\u{6_5}", "u"); + }); + + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestRegexpUnicodeEscape(t *testing.T) { + const SCRIPT = ` + assert.sameValue("u{0_2}".match(/\u{0_2}/)[0], "u{0_2}"); + assert.sameValue("uu\x02".match(/\u{2}/u)[0], '\x02'); + assert.sameValue("uu\x02".match(/\u{2}/)[0], "uu"); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func BenchmarkRegexpSplitWithBackRef(b *testing.B) { + const SCRIPT = ` + "aaaaaaaaaaaaaaaaaaaaaaaaa++bbbbbbbbbbbbbbbbbbbbbb+-ccccccccccccccccccccccc".split(/([+-])\1/) + ` + b.StopTimer() + prg, err := Compile("test.js", SCRIPT, false) + if err != nil { + b.Fatal(err) + } + vm := New() + b.StartTimer() + for i := 0; i < b.N; i++ { + vm.RunProgram(prg) + } +} + +func BenchmarkRegexpMatch(b *testing.B) { + const SCRIPT = ` + "a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + ".match(/[^\r\n]+/g) + ` + b.StopTimer() + prg, err := Compile("test.js", SCRIPT, false) + if err != nil { + b.Fatal(err) + } + vm := New() + b.StartTimer() + for i := 0; i < b.N; i++ { + vm.RunProgram(prg) + } +} + +func BenchmarkRegexpMatchCache(b *testing.B) { + const SCRIPT = ` + (function() { + var s = "a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + " + var r = /[^\r\n]+/g + while(r.exec(s)) {}; + }); + ` + vm := New() + v, err := vm.RunString(SCRIPT) + if err != nil { + b.Fatal(err) + } + if fn, ok := AssertFunction(v); ok { + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + fn(_undefined) + } + } else { + b.Fatal("not a function") + } +} + +func BenchmarkRegexpMatchAll(b *testing.B) { + const SCRIPT = ` + (function() { + var s = "a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + a\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\ra\nb\r\c\nd\r\e\n\f\rg\nh\r\ + " + var r = /[^\r\n]+/g + for (var v of s.matchAll(r)) {} + }); + ` + vm := New() + v, err := vm.RunString(SCRIPT) + if err != nil { + b.Fatal(err) + } + if fn, ok := AssertFunction(v); ok { + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + fn(_undefined) + } + } else { + b.Fatal("not a function") + } +} + +func BenchmarkRegexpSingleExec(b *testing.B) { + vm := New() + regexp := vm.Get("RegExp") + f := func(reStr, str string, b *testing.B) { + r, err := vm.New(regexp, vm.ToValue(reStr)) + if err != nil { + b.Fatal(err) + } + exec, ok := AssertFunction(r.Get("exec")) + if !ok { + b.Fatal("RegExp.exec is not a function") + } + arg := vm.ToValue(str) + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _, err := exec(r, arg) + if err != nil { + b.Fatal(err) + } + } + } + + b.Run("Re-ASCII", func(b *testing.B) { + f("test", "aaaaaaaaaaaaaaaaaaaaaaaaa testing", b) + }) + + b.Run("Re2-ASCII", func(b *testing.B) { + f("(?=)test", "aaaaaaaaaaaaaaaaaaaaaaaaa testing", b) + }) + + b.Run("Re-Unicode", func(b *testing.B) { + f("test", "aaaaaaaaaaaaaaaaaaaaaaaaa testing 😀", b) + }) + + b.Run("Re2-Unicode", func(b *testing.B) { + f("(?=)test", "aaaaaaaaaaaaaaaaaaaaaaaaa testing 😀", b) + }) + +} diff --git a/goja/runtime.go b/goja/runtime.go new file mode 100644 index 0000000..fe88994 --- /dev/null +++ b/goja/runtime.go @@ -0,0 +1,3194 @@ +package goja + +import ( + "bytes" + "errors" + "fmt" + "go/ast" + "hash/maphash" + "math" + "math/big" + "math/bits" + "math/rand" + "reflect" + "runtime" + "strconv" + "time" + + "golang.org/x/text/collate" + + js_ast "github.com/dop251/goja/ast" + "github.com/dop251/goja/file" + "github.com/dop251/goja/parser" + "github.com/dop251/goja/unistring" +) + +const ( + sqrt1_2 float64 = math.Sqrt2 / 2 + + deoptimiseRegexp = false +) + +var ( + typeCallable = reflect.TypeOf(Callable(nil)) + typeValue = reflect.TypeOf((*Value)(nil)).Elem() + typeObject = reflect.TypeOf((*Object)(nil)) + typeTime = reflect.TypeOf(time.Time{}) + typeBigInt = reflect.TypeOf((*big.Int)(nil)) + typeBytes = reflect.TypeOf(([]byte)(nil)) +) + +type iterationKind int + +const ( + iterationKindKey iterationKind = iota + iterationKindValue + iterationKindKeyValue +) + +type global struct { + stash stash + + Object *Object + Array *Object + Function *Object + String *Object + Number *Object + BigInt *Object + Boolean *Object + RegExp *Object + Date *Object + Symbol *Object + Proxy *Object + Reflect *Object + Promise *Object + Math *Object + JSON *Object + + AsyncFunction *Object + + ArrayBuffer *Object + DataView *Object + TypedArray *Object + Uint8Array *Object + Uint8ClampedArray *Object + Int8Array *Object + Uint16Array *Object + Int16Array *Object + Uint32Array *Object + Int32Array *Object + Float32Array *Object + Float64Array *Object + BigInt64Array *Object + BigUint64Array *Object + + WeakSet *Object + WeakMap *Object + Map *Object + Set *Object + + Error *Object + AggregateError *Object + TypeError *Object + ReferenceError *Object + SyntaxError *Object + RangeError *Object + EvalError *Object + URIError *Object + + GoError *Object + + ObjectPrototype *Object + ArrayPrototype *Object + NumberPrototype *Object + BigIntPrototype *Object + StringPrototype *Object + BooleanPrototype *Object + FunctionPrototype *Object + RegExpPrototype *Object + DatePrototype *Object + SymbolPrototype *Object + + ArrayBufferPrototype *Object + DataViewPrototype *Object + TypedArrayPrototype *Object + WeakSetPrototype *Object + WeakMapPrototype *Object + MapPrototype *Object + SetPrototype *Object + PromisePrototype *Object + + GeneratorFunctionPrototype *Object + GeneratorFunction *Object + GeneratorPrototype *Object + + AsyncFunctionPrototype *Object + + IteratorPrototype *Object + ArrayIteratorPrototype *Object + MapIteratorPrototype *Object + SetIteratorPrototype *Object + StringIteratorPrototype *Object + RegExpStringIteratorPrototype *Object + + ErrorPrototype *Object + + Eval *Object + + thrower *Object + + stdRegexpProto *guardedObject + + weakSetAdder *Object + weakMapAdder *Object + mapAdder *Object + setAdder *Object + arrayValues *Object + arrayToString *Object + + stringproto_trimEnd *Object + stringproto_trimStart *Object + + parseFloat, parseInt *Object + + typedArrayValues *Object +} + +type Flag int + +const ( + FLAG_NOT_SET Flag = iota + FLAG_FALSE + FLAG_TRUE +) + +func (f Flag) Bool() bool { + return f == FLAG_TRUE +} + +func ToFlag(b bool) Flag { + if b { + return FLAG_TRUE + } + return FLAG_FALSE +} + +type RandSource func() float64 + +type Now func() time.Time + +type Runtime struct { + global global + globalObject *Object + stringSingleton *stringObject + rand RandSource + now Now + _collator *collate.Collator + parserOptions []parser.Option + + symbolRegistry map[unistring.String]*Symbol + + fieldsInfoCache map[reflect.Type]*reflectFieldsInfo + methodsInfoCache map[reflect.Type]*reflectMethodsInfo + + fieldNameMapper FieldNameMapper + + vm *vm + hash *maphash.Hash + idSeq uint64 + + jobQueue []func() + + promiseRejectionTracker PromiseRejectionTracker + asyncContextTracker AsyncContextTracker +} + +type StackFrame struct { + prg *Program + funcName unistring.String + pc int +} + +func (f *StackFrame) SrcName() string { + if f.prg == nil { + return "" + } + return f.prg.src.Name() +} + +func (f *StackFrame) FuncName() string { + if f.funcName == "" && f.prg == nil { + return "" + } + if f.funcName == "" { + return "" + } + return f.funcName.String() +} + +func (f *StackFrame) Position() file.Position { + if f.prg == nil || f.prg.src == nil { + return file.Position{} + } + return f.prg.src.Position(f.prg.sourceOffset(f.pc)) +} + +func (f *StackFrame) WriteToValueBuilder(b *StringBuilder) { + if f.prg != nil { + if n := f.prg.funcName; n != "" { + b.WriteString(stringValueFromRaw(n)) + b.writeASCII(" (") + } + p := f.Position() + if p.Filename != "" { + b.WriteUTF8String(p.Filename) + } else { + b.writeASCII("") + } + b.WriteRune(':') + b.writeASCII(strconv.Itoa(p.Line)) + b.WriteRune(':') + b.writeASCII(strconv.Itoa(p.Column)) + b.WriteRune('(') + b.writeASCII(strconv.Itoa(f.pc)) + b.WriteRune(')') + if f.prg.funcName != "" { + b.WriteRune(')') + } + } else { + if f.funcName != "" { + b.WriteString(stringValueFromRaw(f.funcName)) + b.writeASCII(" (") + } + b.writeASCII("native") + if f.funcName != "" { + b.WriteRune(')') + } + } +} + +func (f *StackFrame) Write(b *bytes.Buffer) { + if f.prg != nil { + if n := f.prg.funcName; n != "" { + b.WriteString(n.String()) + b.WriteString(" (") + } + p := f.Position() + if p.Filename != "" { + b.WriteString(p.Filename) + } else { + b.WriteString("") + } + b.WriteByte(':') + b.WriteString(strconv.Itoa(p.Line)) + b.WriteByte(':') + b.WriteString(strconv.Itoa(p.Column)) + b.WriteByte('(') + b.WriteString(strconv.Itoa(f.pc)) + b.WriteByte(')') + if f.prg.funcName != "" { + b.WriteByte(')') + } + } else { + if f.funcName != "" { + b.WriteString(f.funcName.String()) + b.WriteString(" (") + } + b.WriteString("native") + if f.funcName != "" { + b.WriteByte(')') + } + } +} + +// An un-catchable exception is not catchable by try/catch statements (finally is not executed either), +// but it is returned as an error to a Go caller rather than causing a panic. +type uncatchableException interface { + error + _uncatchableException() +} + +type Exception struct { + val Value + stack []StackFrame +} + +type baseUncatchableException struct { + Exception +} + +func (e *baseUncatchableException) _uncatchableException() {} + +type InterruptedError struct { + baseUncatchableException + iface interface{} +} + +func (e *InterruptedError) Unwrap() error { + if err, ok := e.iface.(error); ok { + return err + } + return nil +} + +type StackOverflowError struct { + baseUncatchableException +} + +func (e *InterruptedError) Value() interface{} { + return e.iface +} + +func (e *InterruptedError) String() string { + if e == nil { + return "" + } + var b bytes.Buffer + if e.iface != nil { + b.WriteString(fmt.Sprint(e.iface)) + b.WriteByte('\n') + } + e.writeFullStack(&b) + return b.String() +} + +func (e *InterruptedError) Error() string { + if e == nil || e.iface == nil { + return "" + } + var b bytes.Buffer + b.WriteString(fmt.Sprint(e.iface)) + e.writeShortStack(&b) + return b.String() +} + +func (e *Exception) writeFullStack(b *bytes.Buffer) { + for _, frame := range e.stack { + b.WriteString("\tat ") + frame.Write(b) + b.WriteByte('\n') + } +} + +func (e *Exception) writeShortStack(b *bytes.Buffer) { + if len(e.stack) > 0 && (e.stack[0].prg != nil || e.stack[0].funcName != "") { + b.WriteString(" at ") + e.stack[0].Write(b) + } +} + +func (e *Exception) String() string { + if e == nil { + return "" + } + var b bytes.Buffer + if e.val != nil { + b.WriteString(e.val.String()) + b.WriteByte('\n') + } + e.writeFullStack(&b) + return b.String() +} + +func (e *Exception) Error() string { + if e == nil { + return "" + } + var b bytes.Buffer + if e.val != nil { + b.WriteString(e.val.String()) + } + e.writeShortStack(&b) + return b.String() +} + +func (e *Exception) Value() Value { + return e.val +} + +func (e *Exception) Unwrap() error { + if obj, ok := e.val.(*Object); ok { + if obj.runtime.getGoError().self.hasInstance(obj) { + if val := obj.Get("value"); val != nil { + e1, _ := val.Export().(error) + return e1 + } + } + } + return nil +} + +func (e *Exception) Stack() []StackFrame { + return e.stack +} + +func (r *Runtime) createIterProto(val *Object) objectImpl { + o := newBaseObjectObj(val, r.global.ObjectPrototype, classObject) + + o._putSym(SymIterator, valueProp(r.newNativeFunc(r.returnThis, "[Symbol.iterator]", 0), true, false, true)) + return o +} + +func (r *Runtime) getIteratorPrototype() *Object { + var o *Object + if o = r.global.IteratorPrototype; o == nil { + o = &Object{runtime: r} + r.global.IteratorPrototype = o + o.self = r.createIterProto(o) + } + return o +} + +func (r *Runtime) init() { + r.rand = rand.Float64 + r.now = time.Now + + r.global.ObjectPrototype = &Object{runtime: r} + r.newTemplatedObject(getObjectProtoTemplate(), r.global.ObjectPrototype) + + r.globalObject = &Object{runtime: r} + r.newTemplatedObject(getGlobalObjectTemplate(), r.globalObject) + + r.vm = &vm{ + r: r, + } + r.vm.init() +} + +func (r *Runtime) typeErrorResult(throw bool, args ...interface{}) { + if throw { + panic(r.NewTypeError(args...)) + } +} + +func (r *Runtime) newError(typ *Object, format string, args ...interface{}) Value { + var msg string + if len(args) > 0 { + msg = fmt.Sprintf(format, args...) + } else { + msg = format + } + return r.builtin_new(typ, []Value{newStringValue(msg)}) +} + +func (r *Runtime) throwReferenceError(name unistring.String) { + panic(r.newReferenceError(name)) +} + +func (r *Runtime) newReferenceError(name unistring.String) Value { + return r.newError(r.getReferenceError(), "%s is not defined", name) +} + +func (r *Runtime) newSyntaxError(msg string, offset int) Value { + return r.builtin_new(r.getSyntaxError(), []Value{newStringValue(msg)}) +} + +func newBaseObjectObj(obj, proto *Object, class string) *baseObject { + o := &baseObject{ + class: class, + val: obj, + extensible: true, + prototype: proto, + } + obj.self = o + o.init() + return o +} + +func newGuardedObj(proto *Object, class string) *guardedObject { + return &guardedObject{ + baseObject: baseObject{ + class: class, + extensible: true, + prototype: proto, + }, + } +} + +func (r *Runtime) newBaseObject(proto *Object, class string) (o *baseObject) { + v := &Object{runtime: r} + return newBaseObjectObj(v, proto, class) +} + +func (r *Runtime) newGuardedObject(proto *Object, class string) (o *guardedObject) { + v := &Object{runtime: r} + o = newGuardedObj(proto, class) + v.self = o + o.val = v + o.init() + return +} + +func (r *Runtime) NewObject() (v *Object) { + return r.newBaseObject(r.global.ObjectPrototype, classObject).val +} + +// CreateObject creates an object with given prototype. Equivalent of Object.create(proto). +func (r *Runtime) CreateObject(proto *Object) *Object { + return r.newBaseObject(proto, classObject).val +} + +func (r *Runtime) NewArray(items ...interface{}) *Object { + values := make([]Value, len(items)) + for i, item := range items { + values[i] = r.ToValue(item) + } + return r.newArrayValues(values) +} + +func (r *Runtime) NewTypeError(args ...interface{}) *Object { + msg := "" + if len(args) > 0 { + f, _ := args[0].(string) + msg = fmt.Sprintf(f, args[1:]...) + } + return r.builtin_new(r.getTypeError(), []Value{newStringValue(msg)}) +} + +func (r *Runtime) NewGoError(err error) *Object { + e := r.newError(r.getGoError(), err.Error()).(*Object) + e.Set("value", err) + return e +} + +func (r *Runtime) newFunc(name unistring.String, length int, strict bool) (f *funcObject) { + f = &funcObject{} + r.initBaseJsFunction(&f.baseJsFuncObject, strict) + f.val.self = f + f.init(name, intToValue(int64(length))) + return +} + +func (r *Runtime) newAsyncFunc(name unistring.String, length int, strict bool) (f *asyncFuncObject) { + f = &asyncFuncObject{} + r.initBaseJsFunction(&f.baseJsFuncObject, strict) + f.class = classFunction + f.prototype = r.getAsyncFunctionPrototype() + f.val.self = f + f.init(name, intToValue(int64(length))) + return +} + +func (r *Runtime) newGeneratorFunc(name unistring.String, length int, strict bool) (f *generatorFuncObject) { + f = &generatorFuncObject{} + r.initBaseJsFunction(&f.baseJsFuncObject, strict) + f.class = classFunction + f.prototype = r.getGeneratorFunctionPrototype() + f.val.self = f + f.init(name, intToValue(int64(length))) + f._putProp("prototype", r.newBaseObject(r.getGeneratorPrototype(), classObject).val, true, false, false) + return +} + +func (r *Runtime) newClassFunc(name unistring.String, length int, proto *Object, derived bool) (f *classFuncObject) { + v := &Object{runtime: r} + + f = &classFuncObject{} + f.class = classFunction + f.val = v + f.extensible = true + f.strict = true + f.derived = derived + v.self = f + f.prototype = proto + f.init(name, intToValue(int64(length))) + return +} + +func (r *Runtime) initBaseJsFunction(f *baseJsFuncObject, strict bool) { + v := &Object{runtime: r} + + f.class = classFunction + f.val = v + f.extensible = true + f.strict = strict + f.prototype = r.getFunctionPrototype() +} + +func (r *Runtime) newMethod(name unistring.String, length int, strict bool) (f *methodFuncObject) { + f = &methodFuncObject{} + r.initBaseJsFunction(&f.baseJsFuncObject, strict) + f.val.self = f + f.init(name, intToValue(int64(length))) + return +} + +func (r *Runtime) newGeneratorMethod(name unistring.String, length int, strict bool) (f *generatorMethodFuncObject) { + f = &generatorMethodFuncObject{} + r.initBaseJsFunction(&f.baseJsFuncObject, strict) + f.prototype = r.getGeneratorFunctionPrototype() + f.val.self = f + f.init(name, intToValue(int64(length))) + f._putProp("prototype", r.newBaseObject(r.getGeneratorPrototype(), classObject).val, true, false, false) + return +} + +func (r *Runtime) newAsyncMethod(name unistring.String, length int, strict bool) (f *asyncMethodFuncObject) { + f = &asyncMethodFuncObject{} + r.initBaseJsFunction(&f.baseJsFuncObject, strict) + f.val.self = f + f.init(name, intToValue(int64(length))) + return +} + +func (r *Runtime) initArrowFunc(f *arrowFuncObject, strict bool) { + r.initBaseJsFunction(&f.baseJsFuncObject, strict) + f.newTarget = r.vm.newTarget +} + +func (r *Runtime) newArrowFunc(name unistring.String, length int, strict bool) (f *arrowFuncObject) { + f = &arrowFuncObject{} + r.initArrowFunc(f, strict) + f.val.self = f + f.init(name, intToValue(int64(length))) + return +} + +func (r *Runtime) newAsyncArrowFunc(name unistring.String, length int, strict bool) (f *asyncArrowFuncObject) { + f = &asyncArrowFuncObject{} + r.initArrowFunc(&f.arrowFuncObject, strict) + f.class = classObject + f.prototype = r.getAsyncFunctionPrototype() + f.val.self = f + f.init(name, intToValue(int64(length))) + return +} + +func (r *Runtime) newNativeConstructor(call func(ConstructorCall) *Object, name unistring.String, length int64) *Object { + v := &Object{runtime: r} + + f := &nativeFuncObject{ + baseFuncObject: baseFuncObject{ + baseObject: baseObject{ + class: classFunction, + val: v, + extensible: true, + prototype: r.getFunctionPrototype(), + }, + }, + } + + f.f = func(c FunctionCall) Value { + thisObj, _ := c.This.(*Object) + if thisObj != nil { + res := call(ConstructorCall{ + This: thisObj, + Arguments: c.Arguments, + }) + if res == nil { + return _undefined + } + return res + } + return f.defaultConstruct(call, c.Arguments, nil) + } + + f.construct = func(args []Value, newTarget *Object) *Object { + return f.defaultConstruct(call, args, newTarget) + } + + v.self = f + f.init(name, intToValue(length)) + + proto := r.NewObject() + proto.self._putProp("constructor", v, true, false, true) + f._putProp("prototype", proto, true, false, false) + + return v +} + +func (r *Runtime) newNativeConstructOnly(v *Object, ctor func(args []Value, newTarget *Object) *Object, defaultProto *Object, name unistring.String, length int64) *nativeFuncObject { + return r.newNativeFuncAndConstruct(v, func(call FunctionCall) Value { + return ctor(call.Arguments, nil) + }, + func(args []Value, newTarget *Object) *Object { + if newTarget == nil { + newTarget = v + } + return ctor(args, newTarget) + }, defaultProto, name, intToValue(length)) +} + +func (r *Runtime) newNativeFuncAndConstruct(v *Object, call func(call FunctionCall) Value, ctor func(args []Value, newTarget *Object) *Object, defaultProto *Object, name unistring.String, l Value) *nativeFuncObject { + if v == nil { + v = &Object{runtime: r} + } + + f := &nativeFuncObject{ + baseFuncObject: baseFuncObject{ + baseObject: baseObject{ + class: classFunction, + val: v, + extensible: true, + prototype: r.getFunctionPrototype(), + }, + }, + f: call, + construct: ctor, + } + v.self = f + f.init(name, l) + if defaultProto != nil { + f._putProp("prototype", defaultProto, false, false, false) + } + + return f +} + +func (r *Runtime) newNativeFunc(call func(FunctionCall) Value, name unistring.String, length int) *Object { + v := &Object{runtime: r} + + f := &nativeFuncObject{ + baseFuncObject: baseFuncObject{ + baseObject: baseObject{ + class: classFunction, + val: v, + extensible: true, + prototype: r.getFunctionPrototype(), + }, + }, + f: call, + } + v.self = f + f.init(name, intToValue(int64(length))) + return v +} + +func (r *Runtime) newWrappedFunc(value reflect.Value) *Object { + + v := &Object{runtime: r} + + f := &wrappedFuncObject{ + nativeFuncObject: nativeFuncObject{ + baseFuncObject: baseFuncObject{ + baseObject: baseObject{ + class: classFunction, + val: v, + extensible: true, + prototype: r.getFunctionPrototype(), + }, + }, + f: r.wrapReflectFunc(value), + }, + wrapped: value, + } + v.self = f + name := unistring.NewFromString(runtime.FuncForPC(value.Pointer()).Name()) + f.init(name, intToValue(int64(value.Type().NumIn()))) + return v +} + +func (r *Runtime) newNativeFuncConstructObj(v *Object, construct func(args []Value, proto *Object) *Object, name unistring.String, proto *Object, length int) *nativeFuncObject { + f := &nativeFuncObject{ + baseFuncObject: baseFuncObject{ + baseObject: baseObject{ + class: classFunction, + val: v, + extensible: true, + prototype: r.getFunctionPrototype(), + }, + }, + f: r.constructToCall(construct, proto), + construct: r.wrapNativeConstruct(construct, v, proto), + } + + f.init(name, intToValue(int64(length))) + if proto != nil { + f._putProp("prototype", proto, false, false, false) + } + return f +} + +func (r *Runtime) newNativeFuncConstruct(v *Object, construct func(args []Value, proto *Object) *Object, name unistring.String, prototype *Object, length int64) *Object { + return r.newNativeFuncConstructProto(v, construct, name, prototype, r.getFunctionPrototype(), length) +} + +func (r *Runtime) newNativeFuncConstructProto(v *Object, construct func(args []Value, proto *Object) *Object, name unistring.String, prototype, proto *Object, length int64) *Object { + f := &nativeFuncObject{} + f.class = classFunction + f.val = v + f.extensible = true + v.self = f + f.prototype = proto + f.f = r.constructToCall(construct, prototype) + f.construct = r.wrapNativeConstruct(construct, v, prototype) + f.init(name, intToValue(length)) + if prototype != nil { + f._putProp("prototype", prototype, false, false, false) + } + return v +} + +func (r *Runtime) newPrimitiveObject(value Value, proto *Object, class string) *Object { + v := &Object{runtime: r} + + o := &primitiveValueObject{} + o.class = class + o.val = v + o.extensible = true + v.self = o + o.prototype = proto + o.pValue = value + o.init() + return v +} + +func (r *Runtime) builtin_Number(call FunctionCall) Value { + if len(call.Arguments) > 0 { + switch t := call.Arguments[0].(type) { + case *Object: + primValue := t.toPrimitiveNumber() + if bigint, ok := primValue.(*valueBigInt); ok { + return intToValue((*big.Int)(bigint).Int64()) + } + return primValue.ToNumber() + case *valueBigInt: + return intToValue((*big.Int)(t).Int64()) + default: + return t.ToNumber() + } + } else { + return valueInt(0) + } +} + +func (r *Runtime) builtin_newNumber(args []Value, proto *Object) *Object { + var v Value + if len(args) > 0 { + switch t := args[0].(type) { + case *Object: + primValue := t.toPrimitiveNumber() + if bigint, ok := primValue.(*valueBigInt); ok { + v = intToValue((*big.Int)(bigint).Int64()) + } else { + v = primValue.ToNumber() + } + case *valueBigInt: + v = intToValue((*big.Int)(t).Int64()) + default: + v = t.ToNumber() + } + } else { + v = intToValue(0) + } + return r.newPrimitiveObject(v, proto, classNumber) +} + +func (r *Runtime) builtin_Boolean(call FunctionCall) Value { + if len(call.Arguments) > 0 { + if call.Arguments[0].ToBoolean() { + return valueTrue + } else { + return valueFalse + } + } else { + return valueFalse + } +} + +func (r *Runtime) builtin_newBoolean(args []Value, proto *Object) *Object { + var v Value + if len(args) > 0 { + if args[0].ToBoolean() { + v = valueTrue + } else { + v = valueFalse + } + } else { + v = valueFalse + } + return r.newPrimitiveObject(v, proto, classBoolean) +} + +func (r *Runtime) builtin_new(construct *Object, args []Value) *Object { + return r.toConstructor(construct)(args, construct) +} + +func (r *Runtime) builtin_thrower(call FunctionCall) Value { + obj := r.toObject(call.This) + strict := true + switch fn := obj.self.(type) { + case *funcObject: + strict = fn.strict + } + r.typeErrorResult(strict, "'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them") + return nil +} + +func (r *Runtime) eval(srcVal String, direct, strict bool) Value { + src := escapeInvalidUtf16(srcVal) + vm := r.vm + inGlobal := true + if direct { + for s := vm.stash; s != nil; s = s.outer { + if s.isVariable() { + inGlobal = false + break + } + } + } + vm.pushCtx() + funcObj := _undefined + if !direct { + vm.stash = &r.global.stash + vm.privEnv = nil + } else { + if sb := vm.sb; sb > 0 { + funcObj = vm.stack[sb-1] + } + } + p, err := r.compile("", src, strict, inGlobal, r.vm) + if err != nil { + panic(err) + } + + vm.prg = p + vm.pc = 0 + vm.args = 0 + vm.result = _undefined + vm.push(funcObj) + vm.sb = vm.sp + vm.push(nil) // this + ex := vm.runTry() + retval := vm.result + vm.popCtx() + if ex != nil { + panic(ex) + } + vm.sp -= 2 + return retval +} + +func (r *Runtime) builtin_eval(call FunctionCall) Value { + if len(call.Arguments) == 0 { + return _undefined + } + if str, ok := call.Arguments[0].(String); ok { + return r.eval(str, false, false) + } + return call.Arguments[0] +} + +func (r *Runtime) constructToCall(construct func(args []Value, proto *Object) *Object, proto *Object) func(call FunctionCall) Value { + return func(call FunctionCall) Value { + return construct(call.Arguments, proto) + } +} + +func (r *Runtime) wrapNativeConstruct(c func(args []Value, proto *Object) *Object, ctorObj, defProto *Object) func(args []Value, newTarget *Object) *Object { + if c == nil { + return nil + } + return func(args []Value, newTarget *Object) *Object { + var proto *Object + if newTarget != nil { + proto = r.getPrototypeFromCtor(newTarget, ctorObj, defProto) + } else { + proto = defProto + } + return c(args, proto) + } +} + +func (r *Runtime) toCallable(v Value) func(FunctionCall) Value { + if call, ok := r.toObject(v).self.assertCallable(); ok { + return call + } + r.typeErrorResult(true, "Value is not callable: %s", v.toString()) + return nil +} + +func (r *Runtime) checkObjectCoercible(v Value) { + switch v.(type) { + case valueUndefined, valueNull: + r.typeErrorResult(true, "Value is not object coercible") + } +} + +func toInt8(v Value) int8 { + v = v.ToNumber() + if i, ok := v.(valueInt); ok { + return int8(i) + } + + if f, ok := v.(valueFloat); ok { + f := float64(f) + if !math.IsNaN(f) && !math.IsInf(f, 0) { + return int8(int64(f)) + } + } + return 0 +} + +func toUint8(v Value) uint8 { + v = v.ToNumber() + if i, ok := v.(valueInt); ok { + return uint8(i) + } + + if f, ok := v.(valueFloat); ok { + f := float64(f) + if !math.IsNaN(f) && !math.IsInf(f, 0) { + return uint8(int64(f)) + } + } + return 0 +} + +func toUint8Clamp(v Value) uint8 { + v = v.ToNumber() + if i, ok := v.(valueInt); ok { + if i < 0 { + return 0 + } + if i <= 255 { + return uint8(i) + } + return 255 + } + + if num, ok := v.(valueFloat); ok { + num := float64(num) + if !math.IsNaN(num) { + if num < 0 { + return 0 + } + if num > 255 { + return 255 + } + f := math.Floor(num) + f1 := f + 0.5 + if f1 < num { + return uint8(f + 1) + } + if f1 > num { + return uint8(f) + } + r := uint8(f) + if r&1 != 0 { + return r + 1 + } + return r + } + } + return 0 +} + +func toInt16(v Value) int16 { + v = v.ToNumber() + if i, ok := v.(valueInt); ok { + return int16(i) + } + + if f, ok := v.(valueFloat); ok { + f := float64(f) + if !math.IsNaN(f) && !math.IsInf(f, 0) { + return int16(int64(f)) + } + } + return 0 +} + +func toUint16(v Value) uint16 { + v = v.ToNumber() + if i, ok := v.(valueInt); ok { + return uint16(i) + } + + if f, ok := v.(valueFloat); ok { + f := float64(f) + if !math.IsNaN(f) && !math.IsInf(f, 0) { + return uint16(int64(f)) + } + } + return 0 +} + +func toInt32(v Value) int32 { + v = v.ToNumber() + if i, ok := v.(valueInt); ok { + return int32(i) + } + + if f, ok := v.(valueFloat); ok { + f := float64(f) + if !math.IsNaN(f) && !math.IsInf(f, 0) { + return int32(int64(f)) + } + } + return 0 +} + +func toUint32(v Value) uint32 { + v = v.ToNumber() + if i, ok := v.(valueInt); ok { + return uint32(i) + } + + if f, ok := v.(valueFloat); ok { + f := float64(f) + if !math.IsNaN(f) && !math.IsInf(f, 0) { + return uint32(int64(f)) + } + } + return 0 +} + +func toInt64(v Value) int64 { + v = v.ToNumber() + if i, ok := v.(valueInt); ok { + return int64(i) + } + + if f, ok := v.(valueFloat); ok { + f := float64(f) + if !math.IsNaN(f) && !math.IsInf(f, 0) { + return int64(f) + } + } + return 0 +} + +func toUint64(v Value) uint64 { + v = v.ToNumber() + if i, ok := v.(valueInt); ok { + return uint64(i) + } + + if f, ok := v.(valueFloat); ok { + f := float64(f) + if !math.IsNaN(f) && !math.IsInf(f, 0) { + return uint64(int64(f)) + } + } + return 0 +} + +func toInt(v Value) int { + v = v.ToNumber() + if i, ok := v.(valueInt); ok { + return int(i) + } + + if f, ok := v.(valueFloat); ok { + f := float64(f) + if !math.IsNaN(f) && !math.IsInf(f, 0) { + return int(f) + } + } + return 0 +} + +func toUint(v Value) uint { + v = v.ToNumber() + if i, ok := v.(valueInt); ok { + return uint(i) + } + + if f, ok := v.(valueFloat); ok { + f := float64(f) + if !math.IsNaN(f) && !math.IsInf(f, 0) { + return uint(int64(f)) + } + } + return 0 +} + +func toFloat32(v Value) float32 { + return float32(v.ToFloat()) +} + +func toLength(v Value) int64 { + if v == nil { + return 0 + } + i := v.ToInteger() + if i < 0 { + return 0 + } + if i >= maxInt { + return maxInt - 1 + } + return i +} + +func (r *Runtime) toLengthUint32(v Value) uint32 { + var intVal int64 +repeat: + switch num := v.(type) { + case valueInt: + intVal = int64(num) + case valueFloat: + if v != _negativeZero { + if i, ok := floatToInt(float64(num)); ok { + intVal = i + } else { + goto fail + } + } + case String: + v = num.ToNumber() + goto repeat + default: + // Legacy behaviour as specified in https://tc39.es/ecma262/#sec-arraysetlength (see the note) + n2 := toUint32(v) + n1 := v.ToNumber() + if f, ok := n1.(valueFloat); ok { + f := float64(f) + if f != 0 || !math.Signbit(f) { + goto fail + } + } + if n1.ToInteger() != int64(n2) { + goto fail + } + return n2 + } + if intVal >= 0 && intVal <= math.MaxUint32 { + return uint32(intVal) + } +fail: + panic(r.newError(r.getRangeError(), "Invalid array length")) +} + +func toIntStrict(i int64) int { + if bits.UintSize == 32 { + if i > math.MaxInt32 || i < math.MinInt32 { + panic(rangeError("Integer value overflows 32-bit int")) + } + } + return int(i) +} + +func toIntClamp(i int64) int { + if bits.UintSize == 32 { + if i > math.MaxInt32 { + return math.MaxInt32 + } + if i < math.MinInt32 { + return math.MinInt32 + } + } + return int(i) +} + +func (r *Runtime) toIndex(v Value) int { + num := v.ToInteger() + if num >= 0 && num < maxInt { + if bits.UintSize == 32 && num >= math.MaxInt32 { + panic(r.newError(r.getRangeError(), "Index %s overflows int", v.String())) + } + return int(num) + } + panic(r.newError(r.getRangeError(), "Invalid index %s", v.String())) +} + +func (r *Runtime) toBoolean(b bool) Value { + if b { + return valueTrue + } else { + return valueFalse + } +} + +// New creates an instance of a Javascript runtime that can be used to run code. Multiple instances may be created and +// used simultaneously, however it is not possible to pass JS values across runtimes. +func New() *Runtime { + r := &Runtime{} + r.init() + return r +} + +// Compile creates an internal representation of the JavaScript code that can be later run using the Runtime.RunProgram() +// method. This representation is not linked to a runtime in any way and can be run in multiple runtimes (possibly +// at the same time). +func Compile(name, src string, strict bool) (*Program, error) { + return compile(name, src, strict, true, nil) +} + +// CompileAST creates an internal representation of the JavaScript code that can be later run using the Runtime.RunProgram() +// method. This representation is not linked to a runtime in any way and can be run in multiple runtimes (possibly +// at the same time). +func CompileAST(prg *js_ast.Program, strict bool) (*Program, error) { + return compileAST(prg, strict, true, nil) +} + +// MustCompile is like Compile but panics if the code cannot be compiled. +// It simplifies safe initialization of global variables holding compiled JavaScript code. +func MustCompile(name, src string, strict bool) *Program { + prg, err := Compile(name, src, strict) + if err != nil { + panic(err) + } + + return prg +} + +// Parse takes a source string and produces a parsed AST. Use this function if you want to pass options +// to the parser, e.g.: +// +// p, err := Parse("test.js", "var a = true", parser.WithDisableSourceMaps) +// if err != nil { /* ... */ } +// prg, err := CompileAST(p, true) +// // ... +// +// Otherwise use Compile which combines both steps. +func Parse(name, src string, options ...parser.Option) (prg *js_ast.Program, err error) { + prg, err1 := parser.ParseFile(nil, name, src, 0, options...) + if err1 != nil { + // FIXME offset + err = &CompilerSyntaxError{ + CompilerError: CompilerError{ + Message: err1.Error(), + }, + } + } + return +} + +func compile(name, src string, strict, inGlobal bool, evalVm *vm, parserOptions ...parser.Option) (p *Program, err error) { + prg, err := Parse(name, src, parserOptions...) + if err != nil { + return + } + + return compileAST(prg, strict, inGlobal, evalVm) +} + +func compileAST(prg *js_ast.Program, strict, inGlobal bool, evalVm *vm) (p *Program, err error) { + c := newCompiler() + + defer func() { + if x := recover(); x != nil { + p = nil + switch x1 := x.(type) { + case *CompilerSyntaxError: + err = x1 + default: + panic(x) + } + } + }() + + c.compile(prg, strict, inGlobal, evalVm) + p = c.p + return +} + +func (r *Runtime) compile(name, src string, strict, inGlobal bool, evalVm *vm) (p *Program, err error) { + p, err = compile(name, src, strict, inGlobal, evalVm, r.parserOptions...) + if err != nil { + switch x1 := err.(type) { + case *CompilerSyntaxError: + err = &Exception{ + val: r.builtin_new(r.getSyntaxError(), []Value{newStringValue(x1.Error())}), + } + case *CompilerReferenceError: + err = &Exception{ + val: r.newError(r.getReferenceError(), x1.Message), + } // TODO proper message + } + } + return +} + +// RunString executes the given string in the global context. +func (r *Runtime) RunString(str string) (Value, error) { + return r.RunScript("", str) +} + +// RunScript executes the given string in the global context. +func (r *Runtime) RunScript(name, src string) (Value, error) { + p, err := r.compile(name, src, false, true, nil) + + if err != nil { + return nil, err + } + + return r.RunProgram(p) +} + +func isUncatchableException(e error) bool { + for ; e != nil; e = errors.Unwrap(e) { + if _, ok := e.(uncatchableException); ok { + return true + } + } + return false +} + +func asUncatchableException(v interface{}) error { + switch v := v.(type) { + case uncatchableException: + return v + case error: + if isUncatchableException(v) { + return v + } + } + return nil +} + +// RunProgram executes a pre-compiled (see Compile()) code in the global context. +func (r *Runtime) RunProgram(p *Program) (result Value, err error) { + vm := r.vm + recursive := len(vm.callStack) > 0 + defer func() { + if recursive { + vm.sp -= 2 + vm.popCtx() + } else { + vm.callStack = vm.callStack[:len(vm.callStack)-1] + } + if x := recover(); x != nil { + if ex := asUncatchableException(x); ex != nil { + err = ex + if len(vm.callStack) == 0 { + r.leaveAbrupt() + } + } else { + panic(x) + } + } + }() + if recursive { + vm.pushCtx() + vm.stash = &r.global.stash + vm.privEnv = nil + vm.newTarget = nil + vm.args = 0 + sp := vm.sp + vm.stack.expand(sp + 1) + vm.stack[sp] = _undefined // 'callee' + vm.stack[sp+1] = nil // 'this' + vm.sb = sp + 1 + vm.sp = sp + 2 + } else { + vm.callStack = append(vm.callStack, context{}) + } + vm.prg = p + vm.pc = 0 + vm.result = _undefined + ex := vm.runTry() + if ex == nil { + result = r.vm.result + } else { + err = ex + } + if recursive { + vm.clearStack() + } else { + vm.prg = nil + vm.sb = -1 + r.leave() + } + return +} + +// CaptureCallStack appends the current call stack frames to the stack slice (which may be nil) up to the specified depth. +// The most recent frame will be the first one. +// If depth <= 0 or more than the number of available frames, returns the entire stack. +// This method is not safe for concurrent use and should only be called by a Go function that is +// called from a running script. +func (r *Runtime) CaptureCallStack(depth int, stack []StackFrame) []StackFrame { + l := len(r.vm.callStack) + var offset int + if depth > 0 { + offset = l - depth + 1 + if offset < 0 { + offset = 0 + } + } + if stack == nil { + stack = make([]StackFrame, 0, l-offset+1) + } + return r.vm.captureStack(stack, offset) +} + +// Interrupt a running JavaScript. The corresponding Go call will return an *InterruptedError containing v. +// If the interrupt propagates until the stack is empty the currently queued promise resolve/reject jobs will be cleared +// without being executed. This is the same time they would be executed otherwise. +// Note, it only works while in JavaScript code, it does not interrupt native Go functions (which includes all built-ins). +// If the runtime is currently not running, it will be immediately interrupted on the next Run*() call. +// To avoid that use ClearInterrupt() +func (r *Runtime) Interrupt(v interface{}) { + r.vm.Interrupt(v) +} + +// ClearInterrupt resets the interrupt flag. Typically this needs to be called before the runtime +// is made available for re-use if there is a chance it could have been interrupted with Interrupt(). +// Otherwise if Interrupt() was called when runtime was not running (e.g. if it had already finished) +// so that Interrupt() didn't actually trigger, an attempt to use the runtime will immediately cause +// an interruption. It is up to the user to ensure proper synchronisation so that ClearInterrupt() is +// only called when the runtime has finished and there is no chance of a concurrent Interrupt() call. +func (r *Runtime) ClearInterrupt() { + r.vm.ClearInterrupt() +} + +/* +ToValue converts a Go value into a JavaScript value of a most appropriate type. Structural types (such as structs, maps +and slices) are wrapped so that changes are reflected on the original value which can be retrieved using Value.Export(). + +WARNING! These wrapped Go values do not behave in the same way as native ECMAScript values. If you plan to modify +them in ECMAScript, bear in mind the following caveats: + +1. If a regular JavaScript Object is assigned as an element of a wrapped Go struct, map or array, it is +Export()'ed and therefore copied. This may result in an unexpected behaviour in JavaScript: + + m := map[string]interface{}{} + vm.Set("m", m) + vm.RunString(` + var obj = {test: false}; + m.obj = obj; // obj gets Export()'ed, i.e. copied to a new map[string]interface{} and then this map is set as m["obj"] + obj.test = true; // note, m.obj.test is still false + `) + fmt.Println(m["obj"].(map[string]interface{})["test"]) // prints "false" + +2. Be careful with nested non-pointer compound types (structs, slices and arrays) if you modify them in +ECMAScript. Better avoid it at all if possible. One of the fundamental differences between ECMAScript and Go is in +the former all Objects are references whereas in Go you can have a literal struct or array. Consider the following +example: + + type S struct { + Field int + } + + a := []S{{1}, {2}} // slice of literal structs + vm.Set("a", &a) + vm.RunString(` + let tmp = {Field: 1}; + a[0] = tmp; + a[1] = tmp; + tmp.Field = 2; + `) + +In ECMAScript one would expect a[0].Field and a[1].Field to be equal to 2, but this is really not possible +(or at least non-trivial without some complex reference tracking). + +To cover the most common use cases and to avoid excessive memory allocation, the following 'copy-on-change' mechanism +is implemented (for both arrays and structs): + +* When a nested compound value is accessed, the returned ES value becomes a reference to the literal value. +This ensures that things like 'a[0].Field = 1' work as expected and simple access to 'a[0].Field' does not result +in copying of a[0]. + +* The original container ('a' in our case) keeps track of the returned reference value and if a[0] is reassigned +(e.g. by direct assignment, deletion or shrinking the array) the old a[0] is copied and the earlier returned value +becomes a reference to the copy: + + let tmp = a[0]; // no copy, tmp is a reference to a[0] + tmp.Field = 1; // a[0].Field === 1 after this + a[0] = {Field: 2}; // tmp is now a reference to a copy of the old value (with Field === 1) + a[0].Field === 2 && tmp.Field === 1; // true + +* Array value swaps caused by in-place sort (using Array.prototype.sort()) do not count as re-assignments, instead +the references are adjusted to point to the new indices. + +* Assignment to an inner compound value always does a copy (and sometimes type conversion): + + a[1] = tmp; // a[1] is now a copy of tmp + tmp.Field = 3; // does not affect a[1].Field + +3. Non-addressable structs, slices and arrays get copied. This sometimes may lead to a confusion as assigning to +inner fields does not appear to work: + + a1 := []interface{}{S{1}, S{2}} + vm.Set("a1", &a1) + vm.RunString(` + a1[0].Field === 1; // true + a1[0].Field = 2; + a1[0].Field === 2; // FALSE, because what it really did was copy a1[0] set its Field to 2 and immediately drop it + `) + +An alternative would be making a1[0].Field a non-writable property which would probably be more in line with +ECMAScript, however it would require to manually copy the value if it does need to be modified which may be +impractical. + +Note, the same applies to slices. If a slice is passed by value (not as a pointer), resizing the slice does not reflect on the original +value. Moreover, extending the slice may result in the underlying array being re-allocated and copied. +For example: + + a := []interface{}{1} + vm.Set("a", a) + vm.RunString(`a.push(2); a[0] = 0;`) + fmt.Println(a[0]) // prints "1" + +Notes on individual types: + +# Primitive types + +Primitive types (numbers, string, bool) are converted to the corresponding JavaScript primitives. These values +are goroutine-safe and can be transferred between runtimes. + +# Strings + +Because of the difference in internal string representation between ECMAScript (which uses UTF-16) and Go (which uses +UTF-8) conversion from JS to Go may be lossy. In particular, code points that can be part of UTF-16 surrogate pairs +(0xD800-0xDFFF) cannot be represented in UTF-8 unless they form a valid surrogate pair and are replaced with +utf8.RuneError. + +The string value must be a valid UTF-8. If it is not, invalid characters are replaced with utf8.RuneError, but +the behaviour of a subsequent Export() is unspecified (it may return the original value, or a value with replaced +invalid characters). + +# Nil + +Nil is converted to null. + +# Functions + +func(FunctionCall) Value is treated as a native JavaScript function. This increases performance because there are no +automatic argument and return value type conversions (which involves reflect). Attempting to use +the function as a constructor will result in a TypeError. Note: implementations must not retain and use references +to FunctionCall.Arguments after the function returns. + +func(FunctionCall, *Runtime) Value is treated as above, except the *Runtime is also passed as a parameter. + +func(ConstructorCall) *Object is treated as a native constructor, allowing to use it with the new +operator: + + func MyObject(call goja.ConstructorCall) *goja.Object { + // call.This contains the newly created object as per http://www.ecma-international.org/ecma-262/5.1/index.html#sec-13.2.2 + // call.Arguments contain arguments passed to the function + + call.This.Set("method", method) + + //... + + // If return value is a non-nil *Object, it will be used instead of call.This + // This way it is possible to return a Go struct or a map converted + // into goja.Value using ToValue(), however in this case + // instanceof will not work as expected, unless you set the prototype: + // + // instance := &myCustomStruct{} + // instanceValue := vm.ToValue(instance).(*Object) + // instanceValue.SetPrototype(call.This.Prototype()) + // return instanceValue + return nil + } + + runtime.Set("MyObject", MyObject) + +Then it can be used in JS as follows: + + var o = new MyObject(arg); + var o1 = MyObject(arg); // same thing + o instanceof MyObject && o1 instanceof MyObject; // true + +When a native constructor is called directly (without the new operator) its behavior depends on +this value: if it's an Object, it is passed through, otherwise a new one is created exactly as +if it was called with the new operator. In either case call.NewTarget will be nil. + +func(ConstructorCall, *Runtime) *Object is treated as above, except the *Runtime is also passed as a parameter. + +Any other Go function is wrapped so that the arguments are automatically converted into the required Go types and the +return value is converted to a JavaScript value (using this method). If conversion is not possible, a TypeError is +thrown. + +Functions with multiple return values return an Array. If the last return value is an `error` it is not returned but +converted into a JS exception. If the error is *Exception, it is thrown as is, otherwise it's wrapped in a GoEerror. +Note that if there are exactly two return values and the last is an `error`, the function returns the first value as is, +not an Array. + +# Structs + +Structs are converted to Object-like values. Fields and methods are available as properties, their values are +results of this method (ToValue()) applied to the corresponding Go value. + +Field properties are writable and non-configurable. Method properties are non-writable and non-configurable. + +Attempt to define a new property or delete an existing property will fail (throw in strict mode) unless it's a Symbol +property. Symbol properties only exist in the wrapper and do not affect the underlying Go value. +Note that because a wrapper is created every time a property is accessed it may lead to unexpected results such as this: + + type Field struct{ + } + type S struct { + Field *Field + } + var s = S{ + Field: &Field{}, + } + vm := New() + vm.Set("s", &s) + res, err := vm.RunString(` + var sym = Symbol(66); + var field1 = s.Field; + field1[sym] = true; + var field2 = s.Field; + field1 === field2; // true, because the equality operation compares the wrapped values, not the wrappers + field1[sym] === true; // true + field2[sym] === undefined; // also true + `) + +The same applies to values from maps and slices as well. + +# Handling of time.Time + +time.Time does not get special treatment and therefore is converted just like any other `struct` providing access to +all its methods. This is done deliberately instead of converting it to a `Date` because these two types are not fully +compatible: `time.Time` includes zone, whereas JS `Date` doesn't. Doing the conversion implicitly therefore would +result in a loss of information. + +If you need to convert it to a `Date`, it can be done either in JS: + + var d = new Date(goval.UnixNano()/1e6); + +... or in Go: + + now := time.Now() + vm := New() + val, err := vm.New(vm.Get("Date").ToObject(vm), vm.ToValue(now.UnixNano()/1e6)) + if err != nil { + ... + } + vm.Set("d", val) + +Note that Value.Export() for a `Date` value returns time.Time in local timezone. + +# Maps + +Maps with string, integer, or float key types are converted into host objects that largely behave like a JavaScript Object. +One noticeable difference is that the key order is not stable, as with maps in Go. +Keys are converted to strings following the fmt.Sprintf("%v") convention. + +# Maps with methods + +If a map type has at least one method defined, the properties of the resulting Object represent methods, not map keys. +This is because in JavaScript there is no distinction between 'object.key` and `object[key]`, unlike Go. +If access to the map values is required, it can be achieved by defining another method or, if it's not possible, by +defining an external getter function. + +# Slices + +Slices are converted into host objects that behave largely like JavaScript Array. It has the appropriate +prototype and all the usual methods should work. There is, however, a caveat: converted Arrays may not contain holes +(because Go slices cannot). This means that hasOwnProperty(n) always returns `true` if n < length. Deleting an item with +an index < length will set it to a zero value (but the property will remain). Nil slice elements are be converted to +`null`. Accessing an element beyond `length` returns `undefined`. Also see the warning above about passing slices as +values (as opposed to pointers). + +# Arrays + +Arrays are converted similarly to slices, except the resulting Arrays are not resizable (and therefore the 'length' +property is non-writable). + +Any other type is converted to a generic reflect based host object. Depending on the underlying type it behaves similar +to a Number, String, Boolean or Object. + +Note that the underlying type is not lost, calling Export() returns the original Go value. This applies to all +reflect based types. +*/ +func (r *Runtime) ToValue(i interface{}) Value { + return r.toValue(i, reflect.Value{}) +} + +func (r *Runtime) toValue(i interface{}, origValue reflect.Value) Value { + switch i := i.(type) { + case nil: + return _null + case *Object: + if i == nil || i.self == nil { + return _null + } + if i.runtime != nil && i.runtime != r { + panic(r.NewTypeError("Illegal runtime transition of an Object")) + } + return i + case valueContainer: + return i.toValue(r) + case Value: + return i + case string: + if len(i) <= 16 { + if u := unistring.Scan(i); u != nil { + return &importedString{s: i, u: u, scanned: true} + } + return asciiString(i) + } + return &importedString{s: i} + case bool: + if i { + return valueTrue + } else { + return valueFalse + } + case func(FunctionCall) Value: + name := unistring.NewFromString(runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()) + return r.newNativeFunc(i, name, 0) + case func(FunctionCall, *Runtime) Value: + name := unistring.NewFromString(runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()) + return r.newNativeFunc(func(call FunctionCall) Value { + return i(call, r) + }, name, 0) + case func(ConstructorCall) *Object: + name := unistring.NewFromString(runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()) + return r.newNativeConstructor(i, name, 0) + case func(ConstructorCall, *Runtime) *Object: + name := unistring.NewFromString(runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()) + return r.newNativeConstructor(func(call ConstructorCall) *Object { + return i(call, r) + }, name, 0) + case int: + return intToValue(int64(i)) + case int8: + return intToValue(int64(i)) + case int16: + return intToValue(int64(i)) + case int32: + return intToValue(int64(i)) + case int64: + return intToValue(i) + case uint: + if uint64(i) <= math.MaxInt64 { + return intToValue(int64(i)) + } else { + return floatToValue(float64(i)) + } + case uint8: + return intToValue(int64(i)) + case uint16: + return intToValue(int64(i)) + case uint32: + return intToValue(int64(i)) + case uint64: + if i <= math.MaxInt64 { + return intToValue(int64(i)) + } + return floatToValue(float64(i)) + case float32: + return floatToValue(float64(i)) + case float64: + return floatToValue(i) + case *big.Int: + return (*valueBigInt)(new(big.Int).Set(i)) + case map[string]interface{}: + if i == nil { + return _null + } + obj := &Object{runtime: r} + m := &objectGoMapSimple{ + baseObject: baseObject{ + val: obj, + extensible: true, + }, + data: i, + } + obj.self = m + m.init() + return obj + case []interface{}: + return r.newObjectGoSlice(&i, false).val + case *[]interface{}: + if i == nil { + return _null + } + return r.newObjectGoSlice(i, true).val + } + + if !origValue.IsValid() { + origValue = reflect.ValueOf(i) + } + + value := origValue + for value.Kind() == reflect.Ptr { + value = value.Elem() + } + + if !value.IsValid() { + return _null + } + + switch value.Kind() { + case reflect.Map: + if value.Type().NumMethod() == 0 { + switch value.Type().Key().Kind() { + case reflect.String, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float64, reflect.Float32: + + obj := &Object{runtime: r} + m := &objectGoMapReflect{ + objectGoReflect: objectGoReflect{ + baseObject: baseObject{ + val: obj, + extensible: true, + }, + origValue: origValue, + fieldsValue: value, + }, + } + m.init() + obj.self = m + return obj + } + } + case reflect.Array: + obj := &Object{runtime: r} + a := &objectGoArrayReflect{ + objectGoReflect: objectGoReflect{ + baseObject: baseObject{ + val: obj, + }, + origValue: origValue, + fieldsValue: value, + }, + } + a.init() + obj.self = a + return obj + case reflect.Slice: + obj := &Object{runtime: r} + a := &objectGoSliceReflect{ + objectGoArrayReflect: objectGoArrayReflect{ + objectGoReflect: objectGoReflect{ + baseObject: baseObject{ + val: obj, + }, + origValue: origValue, + fieldsValue: value, + }, + }, + } + a.init() + obj.self = a + return obj + case reflect.Func: + return r.newWrappedFunc(value) + } + + obj := &Object{runtime: r} + o := &objectGoReflect{ + baseObject: baseObject{ + val: obj, + }, + origValue: origValue, + fieldsValue: value, + } + obj.self = o + o.init() + return obj +} + +func (r *Runtime) wrapReflectFunc(value reflect.Value) func(FunctionCall) Value { + return func(call FunctionCall) Value { + typ := value.Type() + nargs := typ.NumIn() + var in []reflect.Value + + if l := len(call.Arguments); l < nargs { + // fill missing arguments with zero values + n := nargs + if typ.IsVariadic() { + n-- + } + in = make([]reflect.Value, n) + for i := l; i < n; i++ { + in[i] = reflect.Zero(typ.In(i)) + } + } else { + if l > nargs && !typ.IsVariadic() { + l = nargs + } + in = make([]reflect.Value, l) + } + + for i, a := range call.Arguments { + var t reflect.Type + + n := i + if n >= nargs-1 && typ.IsVariadic() { + if n > nargs-1 { + n = nargs - 1 + } + + t = typ.In(n).Elem() + } else if n > nargs-1 { // ignore extra arguments + break + } else { + t = typ.In(n) + } + + v := reflect.New(t).Elem() + err := r.toReflectValue(a, v, &objectExportCtx{}) + if err != nil { + panic(r.NewTypeError("could not convert function call parameter %d: %v", i, err)) + } + in[i] = v + } + + out := value.Call(in) + if len(out) == 0 { + return _undefined + } + + if last := out[len(out)-1]; last.Type() == reflectTypeError { + if !last.IsNil() { + err := last.Interface().(error) + if _, ok := err.(*Exception); ok { + panic(err) + } + if isUncatchableException(err) { + panic(err) + } + panic(r.NewGoError(err)) + } + out = out[:len(out)-1] + } + + switch len(out) { + case 0: + return _undefined + case 1: + return r.ToValue(out[0].Interface()) + default: + s := make([]interface{}, len(out)) + for i, v := range out { + s[i] = v.Interface() + } + + return r.ToValue(s) + } + } +} + +func (r *Runtime) toReflectValue(v Value, dst reflect.Value, ctx *objectExportCtx) error { + typ := dst.Type() + + if typ == typeValue { + dst.Set(reflect.ValueOf(v)) + return nil + } + + if typ == typeObject { + if obj, ok := v.(*Object); ok { + dst.Set(reflect.ValueOf(obj)) + return nil + } + } + + if typ == typeCallable { + if fn, ok := AssertFunction(v); ok { + dst.Set(reflect.ValueOf(fn)) + return nil + } + } + + et := v.ExportType() + if et == nil || et == reflectTypeNil { + dst.Set(reflect.Zero(typ)) + return nil + } + + kind := typ.Kind() + for i := 0; ; i++ { + if et.AssignableTo(typ) { + ev := reflect.ValueOf(exportValue(v, ctx)) + for ; i > 0; i-- { + ev = ev.Elem() + } + dst.Set(ev) + return nil + } + expKind := et.Kind() + if expKind == kind && et.ConvertibleTo(typ) || expKind == reflect.String && typ == typeBytes { + ev := reflect.ValueOf(exportValue(v, ctx)) + for ; i > 0; i-- { + ev = ev.Elem() + } + dst.Set(ev.Convert(typ)) + return nil + } + if expKind == reflect.Ptr { + et = et.Elem() + } else { + break + } + } + + if typ == typeTime { + if obj, ok := v.(*Object); ok { + if d, ok := obj.self.(*dateObject); ok { + dst.Set(reflect.ValueOf(d.time())) + return nil + } + } + if et.Kind() == reflect.String { + tme, ok := dateParse(v.String()) + if !ok { + return fmt.Errorf("could not convert string %v to %v", v, typ) + } + dst.Set(reflect.ValueOf(tme)) + return nil + } + } + + switch kind { + case reflect.String: + dst.Set(reflect.ValueOf(v.String()).Convert(typ)) + return nil + case reflect.Bool: + dst.Set(reflect.ValueOf(v.ToBoolean()).Convert(typ)) + return nil + case reflect.Int: + dst.Set(reflect.ValueOf(toInt(v)).Convert(typ)) + return nil + case reflect.Int64: + dst.Set(reflect.ValueOf(toInt64(v)).Convert(typ)) + return nil + case reflect.Int32: + dst.Set(reflect.ValueOf(toInt32(v)).Convert(typ)) + return nil + case reflect.Int16: + dst.Set(reflect.ValueOf(toInt16(v)).Convert(typ)) + return nil + case reflect.Int8: + dst.Set(reflect.ValueOf(toInt8(v)).Convert(typ)) + return nil + case reflect.Uint: + dst.Set(reflect.ValueOf(toUint(v)).Convert(typ)) + return nil + case reflect.Uint64: + dst.Set(reflect.ValueOf(toUint64(v)).Convert(typ)) + return nil + case reflect.Uint32: + dst.Set(reflect.ValueOf(toUint32(v)).Convert(typ)) + return nil + case reflect.Uint16: + dst.Set(reflect.ValueOf(toUint16(v)).Convert(typ)) + return nil + case reflect.Uint8: + dst.Set(reflect.ValueOf(toUint8(v)).Convert(typ)) + return nil + case reflect.Float64: + dst.Set(reflect.ValueOf(v.ToFloat()).Convert(typ)) + return nil + case reflect.Float32: + dst.Set(reflect.ValueOf(toFloat32(v)).Convert(typ)) + return nil + case reflect.Slice, reflect.Array: + if o, ok := v.(*Object); ok { + if v, exists := ctx.getTyped(o, typ); exists { + dst.Set(reflect.ValueOf(v)) + return nil + } + return o.self.exportToArrayOrSlice(dst, typ, ctx) + } + case reflect.Map: + if o, ok := v.(*Object); ok { + if v, exists := ctx.getTyped(o, typ); exists { + dst.Set(reflect.ValueOf(v)) + return nil + } + return o.self.exportToMap(dst, typ, ctx) + } + case reflect.Struct: + if o, ok := v.(*Object); ok { + t := reflect.PtrTo(typ) + if v, exists := ctx.getTyped(o, t); exists { + dst.Set(reflect.ValueOf(v).Elem()) + return nil + } + s := dst + ctx.putTyped(o, t, s.Addr().Interface()) + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + if ast.IsExported(field.Name) { + name := field.Name + if r.fieldNameMapper != nil { + name = r.fieldNameMapper.FieldName(typ, field) + } + var v Value + if field.Anonymous { + v = o + } else { + v = o.self.getStr(unistring.NewFromString(name), nil) + } + + if v != nil { + err := r.toReflectValue(v, s.Field(i), ctx) + if err != nil { + return fmt.Errorf("could not convert struct value %v to %v for field %s: %w", v, field.Type, field.Name, err) + } + } + } + } + return nil + } + case reflect.Func: + if fn, ok := AssertFunction(v); ok { + dst.Set(reflect.MakeFunc(typ, r.wrapJSFunc(fn, typ))) + return nil + } + case reflect.Ptr: + if o, ok := v.(*Object); ok { + if v, exists := ctx.getTyped(o, typ); exists { + dst.Set(reflect.ValueOf(v)) + return nil + } + } + if dst.IsNil() { + dst.Set(reflect.New(typ.Elem())) + } + return r.toReflectValue(v, dst.Elem(), ctx) + } + + return fmt.Errorf("could not convert %v to %v", v, typ) +} + +func (r *Runtime) wrapJSFunc(fn Callable, typ reflect.Type) func(args []reflect.Value) (results []reflect.Value) { + return func(args []reflect.Value) (results []reflect.Value) { + var jsArgs []Value + if len(args) > 0 { + if typ.IsVariadic() { + varArg := args[len(args)-1] + args = args[:len(args)-1] + jsArgs = make([]Value, 0, len(args)+varArg.Len()) + for _, arg := range args { + jsArgs = append(jsArgs, r.ToValue(arg.Interface())) + } + for i := 0; i < varArg.Len(); i++ { + jsArgs = append(jsArgs, r.ToValue(varArg.Index(i).Interface())) + } + } else { + jsArgs = make([]Value, len(args)) + for i, arg := range args { + jsArgs[i] = r.ToValue(arg.Interface()) + } + } + } + + numOut := typ.NumOut() + results = make([]reflect.Value, numOut) + res, err := fn(_undefined, jsArgs...) + if err == nil { + if numOut > 0 { + v := reflect.New(typ.Out(0)).Elem() + err = r.toReflectValue(res, v, &objectExportCtx{}) + if err == nil { + results[0] = v + } + } + } + + if err != nil { + if numOut > 0 && typ.Out(numOut-1) == reflectTypeError { + if ex, ok := err.(*Exception); ok { + if exo, ok := ex.val.(*Object); ok { + if v := exo.self.getStr("value", nil); v != nil { + if v.ExportType().AssignableTo(reflectTypeError) { + err = v.Export().(error) + } + } + } + } + results[numOut-1] = reflect.ValueOf(err).Convert(typ.Out(numOut - 1)) + } else { + panic(err) + } + } + + for i, v := range results { + if !v.IsValid() { + results[i] = reflect.Zero(typ.Out(i)) + } + } + + return + } +} + +// ExportTo converts a JavaScript value into the specified Go value. The second parameter must be a non-nil pointer. +// Returns error if conversion is not possible. +// +// Notes on specific cases: +// +// # Empty interface +// +// Exporting to an interface{} results in a value of the same type as Value.Export() would produce. +// +// # Numeric types +// +// Exporting to numeric types uses the standard ECMAScript conversion operations, same as used when assigning +// values to non-clamped typed array items, e.g. https://262.ecma-international.org/#sec-toint32. +// +// # Functions +// +// Exporting to a 'func' creates a strictly typed 'gateway' into an ES function which can be called from Go. +// The arguments are converted into ES values using Runtime.ToValue(). If the func has no return values, +// the return value is ignored. If the func has exactly one return value, it is converted to the appropriate +// type using ExportTo(). If the last return value is 'error', exceptions are caught and returned as *Exception +// (instances of GoError are unwrapped, i.e. their 'value' is returned instead). In all other cases exceptions +// result in a panic. Any extra return values are zeroed. +// +// 'this' value will always be set to 'undefined'. +// +// For a more low-level mechanism see AssertFunction(). +// +// # Map types +// +// An ES Map can be exported into a Go map type. If any exported key value is non-hashable, the operation panics +// (as reflect.Value.SetMapIndex() would). Symbol.iterator is ignored. +// +// Exporting an ES Set into a map type results in the map being populated with (element) -> (zero value) key/value +// pairs. If any value is non-hashable, the operation panics (as reflect.Value.SetMapIndex() would). +// Symbol.iterator is ignored. +// +// Any other Object populates the map with own enumerable non-symbol properties. +// +// # Slice types +// +// Exporting an ES Set into a slice type results in its elements being exported. +// +// Exporting any Object that implements the iterable protocol (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterable_protocol) +// into a slice type results in the slice being populated with the results of the iteration. +// +// Array is treated as iterable (i.e. overwriting Symbol.iterator affects the result). +// +// If an object has a 'length' property and is not a function it is treated as array-like. The resulting slice +// will contain obj[0], ... obj[length-1]. +// +// ArrayBuffer and ArrayBuffer-backed types (i.e. typed arrays and DataView) can be exported into []byte. The result +// is backed by the original data, no copy is performed. +// +// For any other Object an error is returned. +// +// # Array types +// +// Anything that can be exported to a slice type can also be exported to an array type, as long as the lengths +// match. If they do not, an error is returned. +// +// # Proxy +// +// Proxy objects are treated the same way as if they were accessed from ES code in regard to their properties +// (such as 'length' or [Symbol.iterator]). This means exporting them to slice types works, however +// exporting a proxied Map into a map type does not produce its contents, because the Proxy is not recognised +// as a Map. Same applies to a proxied Set. +func (r *Runtime) ExportTo(v Value, target interface{}) error { + tval := reflect.ValueOf(target) + if tval.Kind() != reflect.Ptr || tval.IsNil() { + return errors.New("target must be a non-nil pointer") + } + return r.toReflectValue(v, tval.Elem(), &objectExportCtx{}) +} + +// GlobalObject returns the global object. +func (r *Runtime) GlobalObject() *Object { + return r.globalObject +} + +// Set the specified variable in the global context. +// Equivalent to running "name = value" in non-strict mode. +// The value is first converted using ToValue(). +// Note, this is not the same as GlobalObject().Set(name, value), +// because if a global lexical binding (let or const) exists, it is set instead. +func (r *Runtime) Set(name string, value interface{}) error { + return r.try(func() { + name := unistring.NewFromString(name) + v := r.ToValue(value) + if ref := r.global.stash.getRefByName(name, false); ref != nil { + ref.set(v) + } else { + r.globalObject.self.setOwnStr(name, v, true) + } + }) +} + +// Get the specified variable in the global context. +// Equivalent to dereferencing a variable by name in non-strict mode. If variable is not defined returns nil. +// Note, this is not the same as GlobalObject().Get(name), +// because if a global lexical binding (let or const) exists, it is used instead. +// This method will panic with an *Exception if a JavaScript exception is thrown in the process. Use Runtime.Try to catch these. +func (r *Runtime) Get(name string) Value { + n := unistring.NewFromString(name) + if v, exists := r.global.stash.getByName(n); exists { + return v + } else { + return r.globalObject.self.getStr(n, nil) + } +} + +// SetRandSource sets random source for this Runtime. If not called, the default math/rand is used. +func (r *Runtime) SetRandSource(source RandSource) { + r.rand = source +} + +// SetTimeSource sets the current time source for this Runtime. +// If not called, the default time.Now() is used. +func (r *Runtime) SetTimeSource(now Now) { + r.now = now +} + +// SetParserOptions sets parser options to be used by RunString, RunScript and eval() within the code. +func (r *Runtime) SetParserOptions(opts ...parser.Option) { + r.parserOptions = opts +} + +// SetMaxCallStackSize sets the maximum function call depth. When exceeded, a *StackOverflowError is thrown and +// returned by RunProgram or by a Callable call. This is useful to prevent memory exhaustion caused by an +// infinite recursion. The default value is math.MaxInt32. +// This method (as the rest of the Set* methods) is not safe for concurrent use and may only be called +// from the vm goroutine or when the vm is not running. +func (r *Runtime) SetMaxCallStackSize(size int) { + r.vm.maxCallStackSize = size +} + +// New is an equivalent of the 'new' operator allowing to call it directly from Go. +func (r *Runtime) New(construct Value, args ...Value) (o *Object, err error) { + err = r.try(func() { + o = r.builtin_new(r.toObject(construct), args) + }) + return +} + +// Callable represents a JavaScript function that can be called from Go. +type Callable func(this Value, args ...Value) (Value, error) + +// AssertFunction checks if the Value is a function and returns a Callable. +// Note, for classes this returns a callable and a 'true', however calling it will always result in a TypeError. +// For classes use AssertConstructor(). +func AssertFunction(v Value) (Callable, bool) { + if obj, ok := v.(*Object); ok { + if f, ok := obj.self.assertCallable(); ok { + return func(this Value, args ...Value) (ret Value, err error) { + err = obj.runtime.runWrapped(func() { + ret = f(FunctionCall{ + This: this, + Arguments: args, + }) + }) + return + }, true + } + } + return nil, false +} + +// Constructor is a type that can be used to call constructors. The first argument (newTarget) can be nil +// which sets it to the constructor function itself. +type Constructor func(newTarget *Object, args ...Value) (*Object, error) + +// AssertConstructor checks if the Value is a constructor and returns a Constructor. +func AssertConstructor(v Value) (Constructor, bool) { + if obj, ok := v.(*Object); ok { + if ctor := obj.self.assertConstructor(); ctor != nil { + return func(newTarget *Object, args ...Value) (ret *Object, err error) { + err = obj.runtime.runWrapped(func() { + ret = ctor(args, newTarget) + }) + return + }, true + } + } + return nil, false +} + +func (r *Runtime) runWrapped(f func()) (err error) { + defer func() { + if x := recover(); x != nil { + if ex := asUncatchableException(x); ex != nil { + err = ex + if len(r.vm.callStack) == 0 { + r.leaveAbrupt() + } + } else { + panic(x) + } + } + }() + ex := r.vm.try(f) + if ex != nil { + err = ex + } + if len(r.vm.callStack) == 0 { + r.leave() + } else { + r.vm.clearStack() + } + return +} + +// IsUndefined returns true if the supplied Value is undefined. Note, it checks against the real undefined, not +// against the global object's 'undefined' property. +func IsUndefined(v Value) bool { + return v == _undefined +} + +// IsNull returns true if the supplied Value is null. +func IsNull(v Value) bool { + return v == _null +} + +// IsNaN returns true if the supplied value is NaN. +func IsNaN(v Value) bool { + f, ok := v.(valueFloat) + return ok && math.IsNaN(float64(f)) +} + +// IsInfinity returns true if the supplied is (+/-)Infinity +func IsInfinity(v Value) bool { + return v == _positiveInf || v == _negativeInf +} + +// Undefined returns JS undefined value. Note if global 'undefined' property is changed this still returns the original value. +func Undefined() Value { + return _undefined +} + +// Null returns JS null value. +func Null() Value { + return _null +} + +// NaN returns a JS NaN value. +func NaN() Value { + return _NaN +} + +// PositiveInf returns a JS +Inf value. +func PositiveInf() Value { + return _positiveInf +} + +// NegativeInf returns a JS -Inf value. +func NegativeInf() Value { + return _negativeInf +} + +func tryFunc(f func()) (ret interface{}) { + defer func() { + ret = recover() + }() + + f() + return +} + +// Try runs a given function catching and returning any JS exception. Use this method to run any code +// that may throw exceptions (such as Object.Get, Object.String, Object.ToInteger, Object.Export, Runtime.Get, Runtime.InstanceOf, etc.) +// outside the Runtime execution context (i.e. when calling directly from Go, not from a JS function implemented in Go). +func (r *Runtime) Try(f func()) *Exception { + return r.vm.try(f) +} + +func (r *Runtime) try(f func()) error { + if ex := r.vm.try(f); ex != nil { + return ex + } + return nil +} + +func (r *Runtime) toObject(v Value, args ...interface{}) *Object { + if obj, ok := v.(*Object); ok { + return obj + } + if len(args) > 0 { + panic(r.NewTypeError(args...)) + } else { + var s string + if v == nil { + s = "undefined" + } else { + s = v.String() + } + panic(r.NewTypeError("Value is not an object: %s", s)) + } +} + +func (r *Runtime) speciesConstructor(o, defaultConstructor *Object) func(args []Value, newTarget *Object) *Object { + c := o.self.getStr("constructor", nil) + if c != nil && c != _undefined { + c = r.toObject(c).self.getSym(SymSpecies, nil) + } + if c == nil || c == _undefined || c == _null { + c = defaultConstructor + } + return r.toConstructor(c) +} + +func (r *Runtime) speciesConstructorObj(o, defaultConstructor *Object) *Object { + c := o.self.getStr("constructor", nil) + if c != nil && c != _undefined { + c = r.toObject(c).self.getSym(SymSpecies, nil) + } + if c == nil || c == _undefined || c == _null { + return defaultConstructor + } + obj := r.toObject(c) + if obj.self.assertConstructor() == nil { + panic(r.NewTypeError("Value is not a constructor")) + } + return obj +} + +func (r *Runtime) returnThis(call FunctionCall) Value { + return call.This +} + +func createDataProperty(o *Object, p Value, v Value) { + o.defineOwnProperty(p, PropertyDescriptor{ + Writable: FLAG_TRUE, + Enumerable: FLAG_TRUE, + Configurable: FLAG_TRUE, + Value: v, + }, false) +} + +func createDataPropertyOrThrow(o *Object, p Value, v Value) { + o.defineOwnProperty(p, PropertyDescriptor{ + Writable: FLAG_TRUE, + Enumerable: FLAG_TRUE, + Configurable: FLAG_TRUE, + Value: v, + }, true) +} + +func toPropertyKey(key Value) Value { + return key.ToString() +} + +func (r *Runtime) getVStr(v Value, p unistring.String) Value { + o := v.ToObject(r) + return o.self.getStr(p, v) +} + +func (r *Runtime) getV(v Value, p Value) Value { + o := v.ToObject(r) + return o.get(p, v) +} + +type iteratorRecord struct { + iterator *Object + next func(FunctionCall) Value +} + +func (r *Runtime) getIterator(obj Value, method func(FunctionCall) Value) *iteratorRecord { + if method == nil { + method = toMethod(r.getV(obj, SymIterator)) + if method == nil { + panic(r.NewTypeError("object is not iterable")) + } + } + + iter := r.toObject(method(FunctionCall{ + This: obj, + })) + + var next func(FunctionCall) Value + + if obj, ok := iter.self.getStr("next", nil).(*Object); ok { + if call, ok := obj.self.assertCallable(); ok { + next = call + } + } + + return &iteratorRecord{ + iterator: iter, + next: next, + } +} + +func iteratorComplete(iterResult *Object) bool { + return nilSafe(iterResult.self.getStr("done", nil)).ToBoolean() +} + +func iteratorValue(iterResult *Object) Value { + return nilSafe(iterResult.self.getStr("value", nil)) +} + +func (ir *iteratorRecord) iterate(step func(Value)) { + r := ir.iterator.runtime + for { + if ir.next == nil { + panic(r.NewTypeError("iterator.next is missing or not a function")) + } + res := r.toObject(ir.next(FunctionCall{This: ir.iterator})) + if iteratorComplete(res) { + break + } + value := iteratorValue(res) + ret := tryFunc(func() { + step(value) + }) + if ret != nil { + _ = tryFunc(func() { + ir.returnIter() + }) + panic(ret) + } + } +} + +func (ir *iteratorRecord) step() (value Value, ex *Exception) { + r := ir.iterator.runtime + ex = r.vm.try(func() { + res := r.toObject(ir.next(FunctionCall{This: ir.iterator})) + done := iteratorComplete(res) + if !done { + value = iteratorValue(res) + } else { + ir.close() + } + }) + return +} + +func (ir *iteratorRecord) returnIter() { + if ir.iterator == nil { + return + } + retMethod := toMethod(ir.iterator.self.getStr("return", nil)) + if retMethod != nil { + ir.iterator.runtime.toObject(retMethod(FunctionCall{This: ir.iterator})) + } + ir.iterator = nil + ir.next = nil +} + +func (ir *iteratorRecord) close() { + ir.iterator = nil + ir.next = nil +} + +// ForOf is a Go equivalent of for-of loop. The function panics if an exception is thrown at any point +// while iterating, including if the supplied value is not iterable +// (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterable_protocol). +// When using outside of Runtime.Run (i.e. when calling directly from Go code, not from a JS function implemented +// in Go) it must be enclosed in Try. See the example. +func (r *Runtime) ForOf(iterable Value, step func(curValue Value) (continueIteration bool)) { + iter := r.getIterator(iterable, nil) + for { + value, ex := iter.step() + if ex != nil { + panic(ex) + } + if value != nil { + var continueIteration bool + ex := r.vm.try(func() { + continueIteration = step(value) + }) + if ex != nil { + iter.returnIter() + panic(ex) + } + if !continueIteration { + iter.returnIter() + break + } + } else { + break + } + } +} + +func (r *Runtime) createIterResultObject(value Value, done bool) Value { + o := r.NewObject() + o.self.setOwnStr("value", value, false) + o.self.setOwnStr("done", r.toBoolean(done), false) + return o +} + +func (r *Runtime) getHash() *maphash.Hash { + if r.hash == nil { + r.hash = &maphash.Hash{} + } + return r.hash +} + +// called when the top level function returns normally (i.e. control is passed outside the Runtime). +func (r *Runtime) leave() { + var jobs []func() + for len(r.jobQueue) > 0 { + jobs, r.jobQueue = r.jobQueue, jobs[:0] + for _, job := range jobs { + job() + } + } + r.jobQueue = nil + r.vm.stack = nil +} + +// called when the top level function returns (i.e. control is passed outside the Runtime) but it was due to an interrupt +func (r *Runtime) leaveAbrupt() { + r.jobQueue = nil + r.ClearInterrupt() +} + +func nilSafe(v Value) Value { + if v != nil { + return v + } + return _undefined +} + +func isArray(object *Object) bool { + self := object.self + if proxy, ok := self.(*proxyObject); ok { + if proxy.target == nil { + panic(typeError("Cannot perform 'IsArray' on a proxy that has been revoked")) + } + return isArray(proxy.target) + } + switch self.className() { + case classArray: + return true + default: + return false + } +} + +func isRegexp(v Value) bool { + if o, ok := v.(*Object); ok { + matcher := o.self.getSym(SymMatch, nil) + if matcher != nil && matcher != _undefined { + return matcher.ToBoolean() + } + _, reg := o.self.(*regexpObject) + return reg + } + return false +} + +func limitCallArgs(call FunctionCall, n int) FunctionCall { + if len(call.Arguments) > n { + return FunctionCall{This: call.This, Arguments: call.Arguments[:n]} + } else { + return call + } +} + +func shrinkCap(newSize, oldCap int) int { + if oldCap > 8 { + if cap := oldCap / 2; cap >= newSize { + return cap + } + } + return oldCap +} + +func growCap(newSize, oldSize, oldCap int) int { + // Use the same algorithm as in runtime.growSlice + doublecap := oldCap + oldCap + if newSize > doublecap { + return newSize + } else { + if oldSize < 1024 { + return doublecap + } else { + cap := oldCap + // Check 0 < cap to detect overflow + // and prevent an infinite loop. + for 0 < cap && cap < newSize { + cap += cap / 4 + } + // Return the requested cap when + // the calculation overflowed. + if cap <= 0 { + return newSize + } + return cap + } + } +} + +func (r *Runtime) genId() (ret uint64) { + if r.hash == nil { + h := r.getHash() + r.idSeq = h.Sum64() + } + if r.idSeq == 0 { + r.idSeq = 1 + } + ret = r.idSeq + r.idSeq++ + return +} + +func (r *Runtime) setGlobal(name unistring.String, v Value, strict bool) { + if ref := r.global.stash.getRefByName(name, strict); ref != nil { + ref.set(v) + } else { + o := r.globalObject.self + if strict { + if o.hasOwnPropertyStr(name) { + o.setOwnStr(name, v, true) + } else { + r.throwReferenceError(name) + } + } else { + o.setOwnStr(name, v, false) + } + } +} + +func (r *Runtime) trackPromiseRejection(p *Promise, operation PromiseRejectionOperation) { + if r.promiseRejectionTracker != nil { + r.promiseRejectionTracker(p, operation) + } +} + +func (r *Runtime) callJobCallback(job *jobCallback, this Value, args ...Value) Value { + return job.callback(FunctionCall{This: this, Arguments: args}) +} + +func (r *Runtime) invoke(v Value, p unistring.String, args ...Value) Value { + o := v.ToObject(r) + return r.toCallable(o.self.getStr(p, nil))(FunctionCall{This: v, Arguments: args}) +} + +func (r *Runtime) iterableToList(iterable Value, method func(FunctionCall) Value) []Value { + iter := r.getIterator(iterable, method) + var values []Value + iter.iterate(func(item Value) { + values = append(values, item) + }) + return values +} + +func (r *Runtime) putSpeciesReturnThis(o objectImpl) { + o._putSym(SymSpecies, &valueProperty{ + getterFunc: r.newNativeFunc(r.returnThis, "get [Symbol.species]", 0), + accessor: true, + configurable: true, + }) +} + +func strToArrayIdx(s unistring.String) uint32 { + if s == "" { + return math.MaxUint32 + } + l := len(s) + if s[0] == '0' { + if l == 1 { + return 0 + } + return math.MaxUint32 + } + var n uint32 + if l < 10 { + // guaranteed not to overflow + for i := 0; i < len(s); i++ { + c := s[i] + if c < '0' || c > '9' { + return math.MaxUint32 + } + n = n*10 + uint32(c-'0') + } + return n + } + if l > 10 { + // guaranteed to overflow + return math.MaxUint32 + } + c9 := s[9] + if c9 < '0' || c9 > '9' { + return math.MaxUint32 + } + for i := 0; i < 9; i++ { + c := s[i] + if c < '0' || c > '9' { + return math.MaxUint32 + } + n = n*10 + uint32(c-'0') + } + if n >= math.MaxUint32/10+1 { + return math.MaxUint32 + } + n *= 10 + n1 := n + uint32(c9-'0') + if n1 < n { + return math.MaxUint32 + } + + return n1 +} + +func strToInt32(s unistring.String) (int32, bool) { + if s == "" { + return -1, false + } + neg := s[0] == '-' + if neg { + s = s[1:] + } + l := len(s) + if s[0] == '0' { + if l == 1 { + return 0, !neg + } + return -1, false + } + var n uint32 + if l < 10 { + // guaranteed not to overflow + for i := 0; i < len(s); i++ { + c := s[i] + if c < '0' || c > '9' { + return -1, false + } + n = n*10 + uint32(c-'0') + } + } else if l > 10 { + // guaranteed to overflow + return -1, false + } else { + c9 := s[9] + if c9 >= '0' { + if !neg && c9 > '7' || c9 > '8' { + // guaranteed to overflow + return -1, false + } + for i := 0; i < 9; i++ { + c := s[i] + if c < '0' || c > '9' { + return -1, false + } + n = n*10 + uint32(c-'0') + } + if n >= math.MaxInt32/10+1 { + // valid number, but it overflows integer + return 0, false + } + n = n*10 + uint32(c9-'0') + } else { + return -1, false + } + } + if neg { + return int32(-n), true + } + return int32(n), true +} + +func strToInt64(s unistring.String) (int64, bool) { + if s == "" { + return -1, false + } + neg := s[0] == '-' + if neg { + s = s[1:] + } + l := len(s) + if s[0] == '0' { + if l == 1 { + return 0, !neg + } + return -1, false + } + var n uint64 + if l < 19 { + // guaranteed not to overflow + for i := 0; i < len(s); i++ { + c := s[i] + if c < '0' || c > '9' { + return -1, false + } + n = n*10 + uint64(c-'0') + } + } else if l > 19 { + // guaranteed to overflow + return -1, false + } else { + c18 := s[18] + if c18 >= '0' { + if !neg && c18 > '7' || c18 > '8' { + // guaranteed to overflow + return -1, false + } + for i := 0; i < 18; i++ { + c := s[i] + if c < '0' || c > '9' { + return -1, false + } + n = n*10 + uint64(c-'0') + } + if n >= math.MaxInt64/10+1 { + // valid number, but it overflows integer + return 0, false + } + n = n*10 + uint64(c18-'0') + } else { + return -1, false + } + } + if neg { + return int64(-n), true + } + return int64(n), true +} + +func strToInt(s unistring.String) (int, bool) { + if bits.UintSize == 32 { + n, ok := strToInt32(s) + return int(n), ok + } + n, ok := strToInt64(s) + return int(n), ok +} + +// Attempts to convert a string into a canonical integer. +// On success returns (number, true). +// If it was a canonical number, but not an integer returns (0, false). This includes -0 and overflows. +// In all other cases returns (-1, false). +// See https://262.ecma-international.org/#sec-canonicalnumericindexstring +func strToIntNum(s unistring.String) (int, bool) { + n, ok := strToInt64(s) + if n == 0 { + return 0, ok + } + if ok && n >= -maxInt && n <= maxInt { + if bits.UintSize == 32 { + if n > math.MaxInt32 || n < math.MinInt32 { + return 0, false + } + } + return int(n), true + } + str := stringValueFromRaw(s) + if str.ToNumber().toString().SameAs(str) { + return 0, false + } + return -1, false +} + +func strToGoIdx(s unistring.String) int { + if n, ok := strToInt(s); ok { + return n + } + return -1 +} + +func strToIdx64(s unistring.String) int64 { + if n, ok := strToInt64(s); ok { + return n + } + return -1 +} + +func assertCallable(v Value) (func(FunctionCall) Value, bool) { + if obj, ok := v.(*Object); ok { + return obj.self.assertCallable() + } + return nil, false +} + +// InstanceOf is an equivalent of "left instanceof right". +// This method will panic with an *Exception if a JavaScript exception is thrown in the process. Use Runtime.Try to catch these. +func (r *Runtime) InstanceOf(left Value, right *Object) (res bool) { + return instanceOfOperator(left, right) +} + +func (r *Runtime) methodProp(f func(FunctionCall) Value, name unistring.String, nArgs int) Value { + return valueProp(r.newNativeFunc(f, name, nArgs), true, false, true) +} + +func (r *Runtime) getPrototypeFromCtor(newTarget, defCtor, defProto *Object) *Object { + if newTarget == defCtor { + return defProto + } + proto := newTarget.self.getStr("prototype", nil) + if obj, ok := proto.(*Object); ok { + return obj + } + return defProto +} diff --git a/goja/runtime_test.go b/goja/runtime_test.go new file mode 100644 index 0000000..c456cfc --- /dev/null +++ b/goja/runtime_test.go @@ -0,0 +1,3140 @@ +package goja + +import ( + "errors" + "fmt" + "math" + "reflect" + "runtime" + "strconv" + "strings" + "testing" + "time" + + "github.com/dop251/goja/parser" +) + +func TestGlobalObjectProto(t *testing.T) { + const SCRIPT = ` + this instanceof Object + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestUnicodeString(t *testing.T) { + const SCRIPT = ` + var s = "Тест"; + s.length === 4 && s[1] === "е"; + + ` + + testScript(SCRIPT, valueTrue, t) +} + +func Test2TierHierarchyProp(t *testing.T) { + const SCRIPT = ` + var a = {}; + Object.defineProperty(a, "test", { + value: 42, + writable: false, + enumerable: false, + configurable: true + }); + var b = Object.create(a); + var c = Object.create(b); + c.test = 43; + c.test === 42 && !b.hasOwnProperty("test"); + + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestConstStringIter(t *testing.T) { + const SCRIPT = ` + + var count = 0; + + for (var i in "1234") { + for (var j in "1234567") { + count++ + } + } + + count; + ` + + testScript(SCRIPT, intToValue(28), t) +} + +func TestUnicodeConcat(t *testing.T) { + const SCRIPT = ` + + var s = "тест"; + var s1 = "test"; + var s2 = "абвгд"; + + s.concat(s1) === "тестtest" && s.concat(s1, s2) === "тестtestабвгд" && s1.concat(s, s2) === "testтестабвгд" + && s.concat(s2) === "тестабвгд"; + + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestIndexOf(t *testing.T) { + const SCRIPT = ` + + "abc".indexOf("", 4) + ` + + testScript(SCRIPT, intToValue(3), t) +} + +func TestUnicodeIndexOf(t *testing.T) { + const SCRIPT = ` + "абвгд".indexOf("вг", 1) === 2 && '中国'.indexOf('国') === 1 + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestLastIndexOf(t *testing.T) { + const SCRIPT = ` + + "abcabab".lastIndexOf("ab", 3) + ` + + testScript(SCRIPT, intToValue(3), t) +} + +func TestUnicodeLastIndexOf(t *testing.T) { + const SCRIPT = ` + "абвабаб".lastIndexOf("аб", 3) + ` + + testScript(SCRIPT, intToValue(3), t) +} + +func TestUnicodeLastIndexOf1(t *testing.T) { + const SCRIPT = ` + "abꞐcde".lastIndexOf("cd"); + ` + + testScript(SCRIPT, intToValue(3), t) +} + +func TestNumber(t *testing.T) { + const SCRIPT = ` + (new Number(100111122133144155)).toString() + ` + + testScript(SCRIPT, asciiString("100111122133144160"), t) +} + +func TestFractionalNumberToStringRadix(t *testing.T) { + const SCRIPT = ` + (new Number(123.456)).toString(36) + ` + + testScript(SCRIPT, asciiString("3f.gez4w97ry"), t) +} + +func TestNumberFormatRounding(t *testing.T) { + const SCRIPT = ` + assert.sameValue((123.456).toExponential(undefined), "1.23456e+2", "undefined"); + assert.sameValue((0.000001).toPrecision(2), "0.0000010") + assert.sameValue((-7).toPrecision(1), "-7"); + assert.sameValue((-42).toPrecision(1), "-4e+1"); + assert.sameValue((0.000001).toPrecision(1), "0.000001"); + assert.sameValue((123.456).toPrecision(1), "1e+2", "1"); + assert.sameValue((123.456).toPrecision(2), "1.2e+2", "2"); + + var n = new Number("0.000000000000000000001"); // 1e-21 + assert.sameValue((n).toPrecision(1), "1e-21"); + assert.sameValue((25).toExponential(0), "3e+1"); + assert.sameValue((-25).toExponential(0), "-3e+1"); + assert.sameValue((12345).toExponential(3), "1.235e+4"); + assert.sameValue((25.5).toFixed(0), "26"); + assert.sameValue((-25.5).toFixed(0), "-26"); + assert.sameValue((99.9).toFixed(0), "100"); + assert.sameValue((99.99).toFixed(1), "100.0"); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestBinOctalNumbers(t *testing.T) { + const SCRIPT = ` + 0b111; + ` + + testScript(SCRIPT, valueInt(7), t) +} + +func TestSetFunc(t *testing.T) { + const SCRIPT = ` + sum(40, 2); + ` + r := New() + err := r.Set("sum", func(call FunctionCall) Value { + return r.ToValue(call.Argument(0).ToInteger() + call.Argument(1).ToInteger()) + }) + if err != nil { + t.Fatal(err) + } + v, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + if i := v.ToInteger(); i != 42 { + t.Fatalf("Expected 42, got: %d", i) + } +} + +func ExampleRuntime_Set_lexical() { + r := New() + _, err := r.RunString("let x") + if err != nil { + panic(err) + } + err = r.Set("x", 1) + if err != nil { + panic(err) + } + fmt.Print(r.Get("x"), r.GlobalObject().Get("x")) + // Output: 1 +} + +func TestRecursiveRun(t *testing.T) { + // Make sure that a recursive call to Run*() correctly sets the environment and no stash or stack + // corruptions occur. + vm := New() + vm.Set("f", func() (Value, error) { + return vm.RunString("let x = 1; { let z = 100, z1 = 200, z2 = 300, z3 = 400; x = x + z3} x;") + }) + res, err := vm.RunString(` + function f1() { + let x = 2; + eval(''); + { + let y = 3; + let res = f(); + if (x !== 2) { // check for stash corruption + throw new Error("x="+x); + } + if (y !== 3) { // check for stack corruption + throw new Error("y="+y); + } + return res; + } + }; + f1(); + `) + if err != nil { + t.Fatal(err) + } + if !res.SameAs(valueInt(401)) { + t.Fatal(res) + } +} + +func TestRecursiveRunWithNArgs(t *testing.T) { + vm := New() + vm.Set("f", func() (Value, error) { + return vm.RunString(` + { + let a = 0; + let b = 1; + a = 2; // this used to to corrupt b, because its location on the stack was off by vm.args (1 in our case) + b; + } + `) + }) + _, err := vm.RunString(` + (function(arg) { // need an ES function call with an argument to set vm.args + let res = f(); + if (res !== 1) { + throw new Error(res); + } + })(123); + `) + if err != nil { + t.Fatal(err) + } +} + +func TestRecursiveRunCallee(t *testing.T) { + // Make sure that a recursive call to Run*() correctly sets the callee (i.e. stack[sb-1]) + vm := New() + vm.Set("f", func() (Value, error) { + return vm.RunString("this; (() => 1)()") + }) + res, err := vm.RunString(` + f(123, 123); + `) + if err != nil { + t.Fatal(err) + } + if !res.SameAs(valueInt(1)) { + t.Fatal(res) + } +} + +func TestObjectGetSet(t *testing.T) { + const SCRIPT = ` + input.test++; + input; + ` + r := New() + o := r.NewObject() + o.Set("test", 42) + r.Set("input", o) + + v, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + if o1, ok := v.(*Object); ok { + if v1 := o1.Get("test"); v1.Export() != int64(43) { + t.Fatalf("Unexpected test value: %v (%T)", v1, v1.Export()) + } + } +} + +func TestThrowFromNativeFunc(t *testing.T) { + const SCRIPT = ` + var thrown; + try { + f(); + } catch (e) { + thrown = e; + } + thrown; + ` + r := New() + r.Set("f", func(call FunctionCall) Value { + panic(r.ToValue("testError")) + }) + + v, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if !v.Equals(asciiString("testError")) { + t.Fatalf("Unexpected result: %v", v) + } +} + +func TestSetGoFunc(t *testing.T) { + const SCRIPT = ` + f(40, 2) + ` + r := New() + r.Set("f", func(a, b int) int { + return a + b + }) + + v, err := r.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if v.ToInteger() != 42 { + t.Fatalf("Unexpected result: %v", v) + } +} + +func TestSetFuncVariadic(t *testing.T) { + vm := New() + vm.Set("f", func(s string, g ...Value) { + something := g[0].ToObject(vm).Get(s).ToInteger() + if something != 5 { + t.Fatal() + } + }) + _, err := vm.RunString(` + f("something", {something: 5}) + `) + if err != nil { + t.Fatal(err) + } +} + +func TestSetFuncVariadicFuncArg(t *testing.T) { + vm := New() + vm.Set("f", func(s string, g ...Value) { + if f, ok := AssertFunction(g[0]); ok { + v, err := f(nil) + if err != nil { + t.Fatal(err) + } + if v != valueTrue { + t.Fatal(v) + } + } + }) + _, err := vm.RunString(` + f("something", () => true) + `) + if err != nil { + t.Fatal(err) + } +} + +func TestArgsKeys(t *testing.T) { + const SCRIPT = ` + function testArgs2(x, y, z) { + // Properties of the arguments object are enumerable. + return Object.keys(arguments); + } + + testArgs2(1,2).length + ` + + testScript(SCRIPT, intToValue(2), t) +} + +func TestIPowOverflow(t *testing.T) { + const SCRIPT = ` + assert.sameValue(Math.pow(65536, 6), 7.922816251426434e+28); + assert.sameValue(Math.pow(10, 19), 1e19); + assert.sameValue(Math.pow(2097151, 3), 9223358842721534000); + assert.sameValue(Math.pow(2097152, 3), 9223372036854776000); + assert.sameValue(Math.pow(-2097151, 3), -9223358842721534000); + assert.sameValue(Math.pow(-2097152, 3), -9223372036854776000); + assert.sameValue(Math.pow(9007199254740992, 0), 1); + assert.sameValue(Math.pow(-9007199254740992, 0), 1); + assert.sameValue(Math.pow(0, 0), 1); + ` + + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestIPow(t *testing.T) { + if res := ipow(-9223372036854775808, 1); res != -9223372036854775808 { + t.Fatal(res) + } + + if res := ipow(9223372036854775807, 1); res != 9223372036854775807 { + t.Fatal(res) + } + + if res := ipow(-9223372036854775807, 1); res != -9223372036854775807 { + t.Fatal(res) + } + + if res := ipow(9223372036854775807, 0); res != 1 { + t.Fatal(res) + } + + if res := ipow(-9223372036854775807, 0); res != 1 { + t.Fatal(res) + } + + if res := ipow(-9223372036854775808, 0); res != 1 { + t.Fatal(res) + } +} + +func TestInterrupt(t *testing.T) { + const SCRIPT = ` + var i = 0; + for (;;) { + i++; + } + ` + + vm := New() + time.AfterFunc(200*time.Millisecond, func() { + vm.Interrupt("halt") + }) + + _, err := vm.RunString(SCRIPT) + if err == nil { + t.Fatal("Err is nil") + } +} + +func TestRuntime_ExportToNumbers(t *testing.T) { + vm := New() + t.Run("int8/no overflow", func(t *testing.T) { + var i8 int8 + err := vm.ExportTo(vm.ToValue(-123), &i8) + if err != nil { + t.Fatal(err) + } + if i8 != -123 { + t.Fatalf("i8: %d", i8) + } + }) + + t.Run("int8/overflow", func(t *testing.T) { + var i8 int8 + err := vm.ExportTo(vm.ToValue(333), &i8) + if err != nil { + t.Fatal(err) + } + if i8 != 77 { + t.Fatalf("i8: %d", i8) + } + }) + + t.Run("int64/uint64", func(t *testing.T) { + var ui64 uint64 + err := vm.ExportTo(vm.ToValue(-1), &ui64) + if err != nil { + t.Fatal(err) + } + if ui64 != math.MaxUint64 { + t.Fatalf("ui64: %d", ui64) + } + }) + + t.Run("int8/float", func(t *testing.T) { + var i8 int8 + err := vm.ExportTo(vm.ToValue(333.9234), &i8) + if err != nil { + t.Fatal(err) + } + if i8 != 77 { + t.Fatalf("i8: %d", i8) + } + }) + + t.Run("int8/object", func(t *testing.T) { + var i8 int8 + err := vm.ExportTo(vm.NewObject(), &i8) + if err != nil { + t.Fatal(err) + } + if i8 != 0 { + t.Fatalf("i8: %d", i8) + } + }) + + t.Run("int/object_cust_valueOf", func(t *testing.T) { + var i int + obj, err := vm.RunString(` + ({ + valueOf: function() { return 42; } + }) + `) + if err != nil { + t.Fatal(err) + } + err = vm.ExportTo(obj, &i) + if err != nil { + t.Fatal(err) + } + if i != 42 { + t.Fatalf("i: %d", i) + } + }) + + t.Run("float32/no_trunc", func(t *testing.T) { + var f float32 + err := vm.ExportTo(vm.ToValue(1.234567), &f) + if err != nil { + t.Fatal(err) + } + if f != 1.234567 { + t.Fatalf("f: %f", f) + } + }) + + t.Run("float32/trunc", func(t *testing.T) { + var f float32 + err := vm.ExportTo(vm.ToValue(1.234567890), &f) + if err != nil { + t.Fatal(err) + } + if f != float32(1.234567890) { + t.Fatalf("f: %f", f) + } + }) + + t.Run("float64", func(t *testing.T) { + var f float64 + err := vm.ExportTo(vm.ToValue(1.234567), &f) + if err != nil { + t.Fatal(err) + } + if f != 1.234567 { + t.Fatalf("f: %f", f) + } + }) + + t.Run("float32/object", func(t *testing.T) { + var f float32 + err := vm.ExportTo(vm.NewObject(), &f) + if err != nil { + t.Fatal(err) + } + if f == f { // expecting NaN + t.Fatalf("f: %f", f) + } + }) + + t.Run("float64/object", func(t *testing.T) { + var f float64 + err := vm.ExportTo(vm.NewObject(), &f) + if err != nil { + t.Fatal(err) + } + if f == f { // expecting NaN + t.Fatalf("f: %f", f) + } + }) + +} + +func TestRuntime_ExportToSlice(t *testing.T) { + const SCRIPT = ` + var a = [1, 2, 3]; + a; + ` + + vm := New() + v, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + var a []string + err = vm.ExportTo(v, &a) + if err != nil { + t.Fatal(err) + } + if l := len(a); l != 3 { + t.Fatalf("Unexpected len: %d", l) + } + if a[0] != "1" || a[1] != "2" || a[2] != "3" { + t.Fatalf("Unexpected value: %+v", a) + } +} + +func TestRuntime_ExportToMap(t *testing.T) { + const SCRIPT = ` + var m = { + "0": 1, + "1": 2, + "2": 3, + } + m; + ` + + vm := New() + v, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + var m map[int]string + err = vm.ExportTo(v, &m) + if err != nil { + t.Fatal(err) + } + if l := len(m); l != 3 { + t.Fatalf("Unexpected len: %d", l) + } + if m[0] != "1" || m[1] != "2" || m[2] != "3" { + t.Fatalf("Unexpected value: %+v", m) + } +} + +func TestRuntime_ExportToMap1(t *testing.T) { + const SCRIPT = ` + var m = { + "0": 1, + "1": 2, + "2": 3, + } + m; + ` + + vm := New() + v, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + var m map[string]string + err = vm.ExportTo(v, &m) + if err != nil { + t.Fatal(err) + } + if l := len(m); l != 3 { + t.Fatalf("Unexpected len: %d", l) + } + if m["0"] != "1" || m["1"] != "2" || m["2"] != "3" { + t.Fatalf("Unexpected value: %+v", m) + } +} + +func TestRuntime_ExportToStruct(t *testing.T) { + const SCRIPT = ` + var m = { + Test: 1, + } + m; + ` + vm := New() + v, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + var o testGoReflectMethod_O + err = vm.ExportTo(v, &o) + if err != nil { + t.Fatal(err) + } + + if o.Test != "1" { + t.Fatalf("Unexpected value: '%s'", o.Test) + } + +} + +func TestRuntime_ExportToStructPtr(t *testing.T) { + const SCRIPT = ` + var m = { + Test: 1, + } + m; + ` + vm := New() + v, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + var o *testGoReflectMethod_O + err = vm.ExportTo(v, &o) + if err != nil { + t.Fatal(err) + } + + if o.Test != "1" { + t.Fatalf("Unexpected value: '%s'", o.Test) + } + +} + +func TestRuntime_ExportToStructAnonymous(t *testing.T) { + type BaseTestStruct struct { + A int64 + B int64 + } + + type TestStruct struct { + BaseTestStruct + C string + } + + const SCRIPT = ` + var m = { + A: 1, + B: 2, + C: "testC" + } + m; + ` + vm := New() + v, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + test := &TestStruct{} + err = vm.ExportTo(v, test) + if err != nil { + t.Fatal(err) + } + + if test.A != 1 { + t.Fatalf("Unexpected value: '%d'", test.A) + } + if test.B != 2 { + t.Fatalf("Unexpected value: '%d'", test.B) + } + if test.C != "testC" { + t.Fatalf("Unexpected value: '%s'", test.C) + } + +} + +func TestRuntime_ExportToStructFromPtr(t *testing.T) { + vm := New() + v := vm.ToValue(&testGoReflectMethod_O{ + field: "5", + Test: "12", + }) + + var o testGoReflectMethod_O + err := vm.ExportTo(v, &o) + if err != nil { + t.Fatal(err) + } + + if o.Test != "12" { + t.Fatalf("Unexpected value: '%s'", o.Test) + } + if o.field != "5" { + t.Fatalf("Unexpected value for field: '%s'", o.field) + } +} + +func TestRuntime_ExportToStructWithPtrValues(t *testing.T) { + type BaseTestStruct struct { + A int64 + B *int64 + } + + type TestStruct2 struct { + E string + } + + type TestStruct struct { + BaseTestStruct + C *string + D *TestStruct2 + } + + const SCRIPT = ` + var m = { + A: 1, + B: 2, + C: "testC", + D: { + E: "testE", + } + } + m; + ` + vm := New() + v, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + test := &TestStruct{} + err = vm.ExportTo(v, test) + if err != nil { + t.Fatal(err) + } + + if test.A != 1 { + t.Fatalf("Unexpected value: '%d'", test.A) + } + if test.B == nil || *test.B != 2 { + t.Fatalf("Unexpected value: '%v'", test.B) + } + if test.C == nil || *test.C != "testC" { + t.Fatalf("Unexpected value: '%v'", test.C) + } + if test.D == nil || test.D.E != "testE" { + t.Fatalf("Unexpected value: '%s'", test.D.E) + } + +} + +func TestRuntime_ExportToTime(t *testing.T) { + const SCRIPT = ` + var dateStr = "2018-08-13T15:02:13+02:00"; + var str = "test123"; + ` + + vm := New() + _, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + var ti time.Time + err = vm.ExportTo(vm.Get("dateStr"), &ti) + if err != nil { + t.Fatal(err) + } + if ti.Format(time.RFC3339) != "2018-08-13T15:02:13+02:00" { + t.Fatalf("Unexpected value: '%s'", ti.Format(time.RFC3339)) + } + + err = vm.ExportTo(vm.Get("str"), &ti) + if err == nil { + t.Fatal("Expected err to not be nil") + } + + var str string + err = vm.ExportTo(vm.Get("dateStr"), &str) + if err != nil { + t.Fatal(err) + } + if str != "2018-08-13T15:02:13+02:00" { + t.Fatalf("Unexpected value: '%s'", str) + } + + d, err := vm.RunString(`new Date(1000)`) + if err != nil { + t.Fatal(err) + } + + ti = time.Time{} + err = vm.ExportTo(d, &ti) + if err != nil { + t.Fatal(err) + } + + if ti.UnixNano() != 1000*1e6 { + t.Fatal(ti) + } + if ti.Location() != time.Local { + t.Fatalf("Wrong location: %v", ti) + } +} + +func ExampleRuntime_ExportTo_func() { + const SCRIPT = ` + function f(param) { + return +param + 2; + } + ` + + vm := New() + _, err := vm.RunString(SCRIPT) + if err != nil { + panic(err) + } + + var fn func(string) string + err = vm.ExportTo(vm.Get("f"), &fn) + if err != nil { + panic(err) + } + + fmt.Println(fn("40")) // note, _this_ value in the function will be undefined. + // Output: 42 +} + +func ExampleRuntime_ExportTo_funcThrow() { + const SCRIPT = ` + function f(param) { + throw new Error("testing"); + } + ` + + vm := New() + _, err := vm.RunString(SCRIPT) + if err != nil { + panic(err) + } + + var fn func(string) (string, error) + err = vm.ExportTo(vm.Get("f"), &fn) + if err != nil { + panic(err) + } + _, err = fn("") + + fmt.Println(err) + // Output: Error: testing at f (:3:9(3)) +} + +func ExampleRuntime_ExportTo_funcVariadic() { + const SCRIPT = ` + function f(...args) { + return args.join("#"); + } + ` + vm := New() + _, err := vm.RunString(SCRIPT) + if err != nil { + panic(err) + } + + var fn func(args ...interface{}) string + err = vm.ExportTo(vm.Get("f"), &fn) + if err != nil { + panic(err) + } + fmt.Println(fn("a", "b", 42)) + // Output: a#b#42 +} + +func TestRuntime_ExportTo_funcVariadic(t *testing.T) { + const SCRIPT = ` + function f(...args) { + return args.join("#"); + } + ` + vm := New() + _, err := vm.RunString(SCRIPT) + if err != nil { + panic(err) + } + + t.Run("no args", func(t *testing.T) { + var fn func(args ...any) string + err = vm.ExportTo(vm.Get("f"), &fn) + if err != nil { + panic(err) + } + res := fn() + if res != "" { + t.Fatal(res) + } + }) + + t.Run("non-variadic args", func(t *testing.T) { + var fn func(firstArg any, args ...any) string + err = vm.ExportTo(vm.Get("f"), &fn) + if err != nil { + panic(err) + } + res := fn("first") + if res != "first" { + t.Fatal(res) + } + }) + + t.Run("non-variadic and variadic args", func(t *testing.T) { + var fn func(firstArg any, args ...any) string + err = vm.ExportTo(vm.Get("f"), &fn) + if err != nil { + panic(err) + } + res := fn("first", "second") + if res != "first#second" { + t.Fatal(res) + } + }) + +} + +func TestRuntime_ExportToFuncFail(t *testing.T) { + const SCRIPT = ` + function f(param) { + return +param + 2; + } + ` + + type T struct { + Field1 int + } + + var fn func(string) (T, error) + + vm := New() + _, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + err = vm.ExportTo(vm.Get("f"), &fn) + if err != nil { + t.Fatal(err) + } + + if _, err := fn("40"); err == nil { + t.Fatal("Expected error") + } +} + +func TestRuntime_ExportToCallable(t *testing.T) { + const SCRIPT = ` + function f(param) { + return +param + 2; + } + ` + vm := New() + _, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + var c Callable + err = vm.ExportTo(vm.Get("f"), &c) + if err != nil { + t.Fatal(err) + } + + res, err := c(Undefined(), vm.ToValue("40")) + if err != nil { + t.Fatal(err) + } else if !res.StrictEquals(vm.ToValue(42)) { + t.Fatalf("Unexpected value: %v", res) + } +} + +func TestRuntime_ExportToObject(t *testing.T) { + const SCRIPT = ` + var o = {"test": 42}; + o; + ` + vm := New() + _, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + var o *Object + err = vm.ExportTo(vm.Get("o"), &o) + if err != nil { + t.Fatal(err) + } + + if v := o.Get("test"); !v.StrictEquals(vm.ToValue(42)) { + t.Fatalf("Unexpected value: %v", v) + } +} + +func ExampleAssertFunction() { + vm := New() + _, err := vm.RunString(` + function sum(a, b) { + return a+b; + } + `) + if err != nil { + panic(err) + } + sum, ok := AssertFunction(vm.Get("sum")) + if !ok { + panic("Not a function") + } + + res, err := sum(Undefined(), vm.ToValue(40), vm.ToValue(2)) + if err != nil { + panic(err) + } + fmt.Println(res) + // Output: 42 +} + +func TestGoFuncError(t *testing.T) { + const SCRIPT = ` + try { + f(); + } catch (e) { + if (!(e instanceof GoError)) { + throw(e); + } + if (e.value.Error() !== "Test") { + throw("Unexpected value: " + e.value.Error()); + } + } + ` + + f := func() error { + return errors.New("Test") + } + + vm := New() + vm.Set("f", f) + _, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } +} + +func TestToValueNil(t *testing.T) { + type T struct{} + var a *T + vm := New() + + if v := vm.ToValue(nil); !IsNull(v) { + t.Fatalf("nil: %v", v) + } + + if v := vm.ToValue(a); !IsNull(v) { + t.Fatalf("struct ptr: %v", v) + } + + var m map[string]interface{} + if v := vm.ToValue(m); !IsNull(v) { + t.Fatalf("map[string]interface{}: %v", v) + } + + var ar []interface{} + if v := vm.ToValue(ar); IsNull(v) { + t.Fatalf("[]interface{}: %v", v) + } + + var arptr *[]interface{} + if v := vm.ToValue(arptr); !IsNull(v) { + t.Fatalf("*[]interface{}: %v", v) + } +} + +func TestToValueFloat(t *testing.T) { + vm := New() + vm.Set("f64", float64(123)) + vm.Set("f32", float32(321)) + + v, err := vm.RunString("f64 === 123 && f32 === 321") + if err != nil { + t.Fatal(err) + } + if v.Export().(bool) != true { + t.Fatalf("StrictEquals for golang float failed") + } +} + +func TestToValueInterface(t *testing.T) { + + f := func(i interface{}) bool { + return i == t + } + vm := New() + vm.Set("f", f) + vm.Set("t", t) + v, err := vm.RunString(`f(t)`) + if err != nil { + t.Fatal(err) + } + if v != valueTrue { + t.Fatalf("v: %v", v) + } +} + +func TestJSONEscape(t *testing.T) { + const SCRIPT = ` + var a = "\\+1"; + JSON.stringify(a); + ` + + testScript(SCRIPT, asciiString(`"\\+1"`), t) +} + +func TestJSONObjectInArray(t *testing.T) { + const SCRIPT = ` + var a = "[{\"a\":1},{\"a\":2}]"; + JSON.stringify(JSON.parse(a)) == a; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestJSONQuirkyNumbers(t *testing.T) { + const SCRIPT = ` + var s; + s = JSON.stringify(NaN); + if (s != "null") { + throw new Error("NaN: " + s); + } + + s = JSON.stringify(Infinity); + if (s != "null") { + throw new Error("Infinity: " + s); + } + + s = JSON.stringify(-Infinity); + if (s != "null") { + throw new Error("-Infinity: " + s); + } + + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestJSONNil(t *testing.T) { + const SCRIPT = ` + JSON.stringify(i); + ` + + vm := New() + var i interface{} + vm.Set("i", i) + ret, err := vm.RunString(SCRIPT) + if err != nil { + t.Fatal(err) + } + + if ret.String() != "null" { + t.Fatalf("Expected 'null', got: %v", ret) + } +} + +type customJsonEncodable struct{} + +func (*customJsonEncodable) JsonEncodable() interface{} { + return "Test" +} + +func TestJsonEncodable(t *testing.T) { + var s customJsonEncodable + + vm := New() + vm.Set("s", &s) + + ret, err := vm.RunString("JSON.stringify(s)") + if err != nil { + t.Fatal(err) + } + if !ret.StrictEquals(vm.ToValue("\"Test\"")) { + t.Fatalf("Expected \"Test\", got: %v", ret) + } +} + +func TestSortComparatorReturnValues(t *testing.T) { + const SCRIPT = ` + var a = []; + for (var i = 0; i < 12; i++) { + a[i] = i; + } + + a.sort(function(x, y) { return y - x }); + + for (var i = 0; i < 12; i++) { + if (a[i] !== 11-i) { + throw new Error("Value at index " + i + " is incorrect: " + a[i]); + } + } + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestSortComparatorReturnValueFloats(t *testing.T) { + const SCRIPT = ` + var a = [ + 5.97, + 9.91, + 4.13, + 9.28, + 3.29, + ]; + a.sort( function(a, b) { return a - b; } ); + for (var i = 1; i < a.length; i++) { + if (a[i] < a[i-1]) { + throw new Error("Array is not sorted: " + a); + } + } + ` + testScript(SCRIPT, _undefined, t) +} + +func TestSortComparatorReturnValueNegZero(t *testing.T) { + const SCRIPT = ` + var a = [2, 1]; + a.sort( function(a, b) { return a > b ? 0 : -0; } ); + for (var i = 1; i < a.length; i++) { + if (a[i] < a[i-1]) { + throw new Error("Array is not sorted: " + a); + } + } + ` + testScript(SCRIPT, _undefined, t) +} + +func TestNilApplyArg(t *testing.T) { + const SCRIPT = ` + (function x(a, b) { + return a === undefined && b === 1; + }).apply(this, [,1]) + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestNilCallArg(t *testing.T) { + const SCRIPT = ` + "use strict"; + function f(a) { + return this === undefined && a === undefined; + } + ` + vm := New() + prg := MustCompile("test.js", SCRIPT, false) + vm.RunProgram(prg) + if f, ok := AssertFunction(vm.Get("f")); ok { + v, err := f(nil, nil) + if err != nil { + t.Fatal(err) + } + if !v.StrictEquals(valueTrue) { + t.Fatalf("Unexpected result: %v", v) + } + } +} + +func TestNullCallArg(t *testing.T) { + const SCRIPT = ` + f(null); + ` + vm := New() + prg := MustCompile("test.js", SCRIPT, false) + vm.Set("f", func(x *int) bool { + return x == nil + }) + + v, err := vm.RunProgram(prg) + if err != nil { + t.Fatal(err) + } + + if !v.StrictEquals(valueTrue) { + t.Fatalf("Unexpected result: %v", v) + } +} + +func TestObjectKeys(t *testing.T) { + const SCRIPT = ` + var o = { a: 1, b: 2, c: 3, d: 4 }; + o; + ` + + vm := New() + prg := MustCompile("test.js", SCRIPT, false) + + res, err := vm.RunProgram(prg) + if err != nil { + t.Fatal(err) + } + + if o, ok := res.(*Object); ok { + keys := o.Keys() + if !reflect.DeepEqual(keys, []string{"a", "b", "c", "d"}) { + t.Fatalf("Unexpected keys: %v", keys) + } + } +} + +func TestReflectCallExtraArgs(t *testing.T) { + const SCRIPT = ` + f(41, "extra") + ` + f := func(x int) int { + return x + 1 + } + + vm := New() + vm.Set("f", f) + + prg := MustCompile("test.js", SCRIPT, false) + + res, err := vm.RunProgram(prg) + if err != nil { + t.Fatal(err) + } + if !res.StrictEquals(intToValue(42)) { + t.Fatalf("Unexpected result: %v", res) + } +} + +func TestReflectCallNotEnoughArgs(t *testing.T) { + const SCRIPT = ` + f(42) + ` + vm := New() + + f := func(x, y int, z *int, s string) (int, error) { + if z != nil { + return 0, fmt.Errorf("z is not nil") + } + if s != "" { + return 0, fmt.Errorf("s is not \"\"") + } + return x + y, nil + } + + vm.Set("f", f) + + prg := MustCompile("test.js", SCRIPT, false) + + res, err := vm.RunProgram(prg) + if err != nil { + t.Fatal(err) + } + if !res.StrictEquals(intToValue(42)) { + t.Fatalf("Unexpected result: %v", res) + } +} + +func TestReflectCallVariadic(t *testing.T) { + const SCRIPT = ` + var r = f("Hello %s, %d", "test", 42); + if (r !== "Hello test, 42") { + throw new Error("test 1 has failed: " + r); + } + + r = f("Hello %s, %s", "test"); + if (r !== "Hello test, %!s(MISSING)") { + throw new Error("test 2 has failed: " + r); + } + + r = f(); + if (r !== "") { + throw new Error("test 3 has failed: " + r); + } + + ` + + vm := New() + vm.Set("f", fmt.Sprintf) + + prg := MustCompile("test.js", SCRIPT, false) + + _, err := vm.RunProgram(prg) + if err != nil { + t.Fatal(err) + } +} + +func TestReflectNullValueArgument(t *testing.T) { + rt := New() + rt.Set("fn", func(v Value) { + if v == nil { + t.Error("null becomes nil") + } + if !IsNull(v) { + t.Error("null is not null") + } + }) + rt.RunString(`fn(null);`) +} + +type testNativeConstructHelper struct { + rt *Runtime + base int64 + // any other state +} + +func (t *testNativeConstructHelper) calc(call FunctionCall) Value { + return t.rt.ToValue(t.base + call.Argument(0).ToInteger()) +} + +func TestNativeConstruct(t *testing.T) { + const SCRIPT = ` + var f = new F(40); + f instanceof F && f.method() === 42 && f.calc(2) === 42; + ` + + rt := New() + + method := func(call FunctionCall) Value { + return rt.ToValue(42) + } + + rt.Set("F", func(call ConstructorCall) *Object { // constructor signature (as opposed to 'func(FunctionCall) Value') + h := &testNativeConstructHelper{ + rt: rt, + base: call.Argument(0).ToInteger(), + } + call.This.Set("method", method) + call.This.Set("calc", h.calc) + return nil // or any other *Object which will be used instead of call.This + }) + + prg := MustCompile("test.js", SCRIPT, false) + + res, err := rt.RunProgram(prg) + if err != nil { + t.Fatal(err) + } + + if !res.StrictEquals(valueTrue) { + t.Fatalf("Unexpected result: %v", res) + } + + if fn, ok := AssertFunction(rt.Get("F")); ok { + v, err := fn(nil, rt.ToValue(42)) + if err != nil { + t.Fatal(err) + } + if o, ok := v.(*Object); ok { + if o.Get("method") == nil { + t.Fatal("No method") + } + } else { + t.Fatal("Not an object") + } + } else { + t.Fatal("Not a function") + } + + resp := &testNativeConstructHelper{} + value := rt.ToValue(resp) + if value.Export() != resp { + t.Fatal("no") + } +} + +func TestCreateObject(t *testing.T) { + const SCRIPT = ` + inst instanceof C; + ` + + r := New() + c := r.ToValue(func(call ConstructorCall) *Object { + return nil + }) + + proto := c.(*Object).Get("prototype").(*Object) + + inst := r.CreateObject(proto) + + r.Set("C", c) + r.Set("inst", inst) + + prg := MustCompile("test.js", SCRIPT, false) + + res, err := r.RunProgram(prg) + if err != nil { + t.Fatal(err) + } + + if !res.StrictEquals(valueTrue) { + t.Fatalf("Unexpected result: %v", res) + } +} + +func TestInterruptInWrappedFunction(t *testing.T) { + rt := New() + v, err := rt.RunString(` + var fn = function() { + while (true) {} + }; + fn; + `) + if err != nil { + t.Fatal(err) + } + fn, ok := AssertFunction(v) + if !ok { + t.Fatal("Not a function") + } + go func() { + <-time.After(10 * time.Millisecond) + rt.Interrupt(errors.New("hi")) + }() + + _, err = fn(nil) + if err == nil { + t.Fatal("expected error") + } + if _, ok := err.(*InterruptedError); !ok { + t.Fatalf("Wrong error type: %T", err) + } +} + +func TestInterruptInWrappedFunction2(t *testing.T) { + rt := New() + // this test panics as otherwise goja will recover and possibly loop + var called bool + rt.Set("v", rt.ToValue(func() { + if called { + go func() { + panic("this should never get called twice") + }() + } + called = true + rt.Interrupt("here is the error") + })) + + rt.Set("s", rt.ToValue(func(a Callable) (Value, error) { + return a(nil) + })) + + rt.Set("k", rt.ToValue(func(e Value) { + go func() { + panic("this should never get called actually") + }() + })) + _, err := rt.RunString(` + Promise.resolve().then(()=>k()); // this should never resolve + while(true) { + try{ + s(() =>{ + v(); + }) + break; + } catch (e) { + k(e); + } + } + `) + if err == nil { + t.Fatal("expected error but got no error") + } + intErr := new(InterruptedError) + if !errors.As(err, &intErr) { + t.Fatalf("Wrong error type: %T", err) + } + if !strings.Contains(intErr.Error(), "here is the error") { + t.Fatalf("Wrong error message: %q", intErr.Error()) + } + _, err = rt.RunString(`Promise.resolve().then(()=>globalThis.S=5)`) + if err != nil { + t.Fatal(err) + } + s := rt.Get("S") + if s == nil || s.ToInteger() != 5 { + t.Fatalf("Wrong value for S %v", s) + } +} + +func TestInterruptInWrappedFunction2Recover(t *testing.T) { + rt := New() + // this test panics as otherwise goja will recover and possibly loop + var vCalled int + rt.Set("v", rt.ToValue(func() { + if vCalled == 0 { + rt.Interrupt("here is the error") + } + vCalled++ + })) + + rt.Set("s", rt.ToValue(func(a Callable) (Value, error) { + v, err := a(nil) + if err != nil { + intErr := new(InterruptedError) + if errors.As(err, &intErr) { + rt.ClearInterrupt() + return nil, errors.New("oops we got interrupted let's not that") + } + } + return v, err + })) + var kCalled int + + rt.Set("k", rt.ToValue(func(e Value) { + kCalled++ + })) + _, err := rt.RunString(` + Promise.resolve().then(()=>k()); + while(true) { + try{ + s(() => { + v(); + }) + break; + } catch (e) { + k(e); + } + } + `) + if err != nil { + t.Fatal(err) + } + if vCalled != 2 { + t.Fatalf("v was not called exactly twice but %d times", vCalled) + } + if kCalled != 2 { + t.Fatalf("k was not called exactly twice but %d times", kCalled) + } + _, err = rt.RunString(`Promise.resolve().then(()=>globalThis.S=5)`) + if err != nil { + t.Fatal(err) + } + s := rt.Get("S") + if s == nil || s.ToInteger() != 5 { + t.Fatalf("Wrong value for S %v", s) + } +} + +func TestInterruptInWrappedFunctionExpectInteruptError(t *testing.T) { + rt := New() + // this test panics as otherwise goja will recover and possibly loop + rt.Set("v", rt.ToValue(func() { + rt.Interrupt("here is the error") + })) + + rt.Set("s", rt.ToValue(func(a Callable) (Value, error) { + return a(nil) + })) + + _, err := rt.RunString(` + s(() =>{ + v(); + }) + `) + if err == nil { + t.Fatal("expected error but got no error") + } + var intErr *InterruptedError + if !errors.As(err, &intErr) { + t.Fatalf("Wrong error type: %T", err) + } + if !strings.Contains(intErr.Error(), "here is the error") { + t.Fatalf("Wrong error message: %q", intErr.Error()) + } +} + +func TestInterruptInWrappedFunctionExpectStackOverflowError(t *testing.T) { + rt := New() + rt.SetMaxCallStackSize(5) + // this test panics as otherwise goja will recover and possibly loop + rt.Set("v", rt.ToValue(func() { + _, err := rt.RunString(` + (function loop() { loop() })(); + `) + if err != nil { + panic(err) + } + })) + + rt.Set("s", rt.ToValue(func(a Callable) (Value, error) { + return a(nil) + })) + + _, err := rt.RunString(` + s(() =>{ + v(); + }) + `) + if err == nil { + t.Fatal("expected error but got no error") + } + var soErr *StackOverflowError + if !errors.As(err, &soErr) { + t.Fatalf("Wrong error type: %T", err) + } +} + +func TestRunLoopPreempt(t *testing.T) { + vm := New() + v, err := vm.RunString("(function() {for (;;) {}})") + if err != nil { + t.Fatal(err) + } + + fn, ok := AssertFunction(v) + if !ok { + t.Fatal("Not a function") + } + + go func() { + <-time.After(100 * time.Millisecond) + runtime.GC() // this hangs if the vm loop does not have any preemption points + vm.Interrupt(errors.New("hi")) + }() + + _, err = fn(nil) + if err == nil { + t.Fatal("expected error") + } + if _, ok := err.(*InterruptedError); !ok { + t.Fatalf("Wrong error type: %T", err) + } +} + +func TestNaN(t *testing.T) { + if !IsNaN(_NaN) { + t.Fatal("IsNaN() doesn't detect NaN") + } + if IsNaN(Undefined()) { + t.Fatal("IsNaN() says undefined is a NaN") + } + if !IsNaN(NaN()) { + t.Fatal("NaN() doesn't return NaN") + } +} + +func TestInf(t *testing.T) { + if !IsInfinity(_positiveInf) { + t.Fatal("IsInfinity() doesn't detect +Inf") + } + if !IsInfinity(_negativeInf) { + t.Fatal("IsInfinity() doesn't detect -Inf") + } + if IsInfinity(Undefined()) { + t.Fatal("IsInfinity() says undefined is a Infinity") + } + if !IsInfinity(PositiveInf()) { + t.Fatal("PositiveInfinity() doesn't return Inf") + } + if !IsInfinity(NegativeInf()) { + t.Fatal("NegativeInfinity() doesn't return Inf") + } +} + +func TestRuntimeNew(t *testing.T) { + vm := New() + v, err := vm.New(vm.Get("Number"), vm.ToValue("12345")) + if err != nil { + t.Fatal(err) + } + if n, ok := v.Export().(int64); ok { + if n != 12345 { + t.Fatalf("n: %v", n) + } + } else { + t.Fatalf("v: %T", v) + } +} + +func TestAutoBoxing(t *testing.T) { + const SCRIPT = ` + function f() { + 'use strict'; + var a = 1; + var thrown1 = false; + var thrown2 = false; + try { + a.test = 42; + } catch (e) { + thrown1 = e instanceof TypeError; + } + try { + a["test1"] = 42; + } catch (e) { + thrown2 = e instanceof TypeError; + } + return thrown1 && thrown2; + } + var a = 1; + a.test = 42; // should not throw + a["test1"] = 42; // should not throw + a.test === undefined && a.test1 === undefined && f(); + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestProtoGetter(t *testing.T) { + const SCRIPT = ` + ({}).__proto__ === Object.prototype && [].__proto__ === Array.prototype; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestSymbol1(t *testing.T) { + const SCRIPT = ` + Symbol.toPrimitive[Symbol.toPrimitive]() === Symbol.toPrimitive; + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestFreezeSymbol(t *testing.T) { + const SCRIPT = ` + var s = Symbol(1); + var o = {}; + o[s] = 42; + Object.freeze(o); + o[s] = 43; + o[s] === 42 && Object.isFrozen(o); + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestToPropertyKey(t *testing.T) { + const SCRIPT = ` + var sym = Symbol(42); + var callCount = 0; + + var wrapper = { + toString: function() { + callCount += 1; + return sym; + }, + valueOf: function() { + $ERROR("valueOf() called"); + } + }; + + var o = {}; + o[wrapper] = function() { return "test" }; + assert.sameValue(o[wrapper], o[sym], "o[wrapper] === o[sym]"); + assert.sameValue(o[wrapper](), "test", "o[wrapper]()"); + assert.sameValue(o[sym](), "test", "o[sym]()"); + + var wrapper1 = {}; + wrapper1[Symbol.toPrimitive] = function(hint) { + if (hint === "string" || hint === "default") { + return "1"; + } + if (hint === "number") { + return 2; + } + $ERROR("Unknown hint value "+hint); + }; + var a = []; + a[wrapper1] = 42; + assert.sameValue(a[1], 42, "a[1]"); + assert.sameValue(a[1], a[wrapper1], "a[1] === a[wrapper1]"); + ` + + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestPrimThisValue(t *testing.T) { + const SCRIPT = ` + function t() { + 'use strict'; + + Boolean.prototype.toString = function() { + return typeof this; + }; + + assert.sameValue(true.toLocaleString(), "boolean"); + + Boolean.prototype[Symbol.iterator] = function() { + return [typeof this][Symbol.iterator](); + } + var s = new Set(true); + assert.sameValue(s.size, 1, "size"); + assert.sameValue(s.has("boolean"), true, "s.has('boolean')"); + } + t(); + ` + + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestPrimThisValueGetter(t *testing.T) { + const SCRIPT = ` + function t() { + 'use strict'; + Object.defineProperty(Boolean.prototype, "toString", { + get: function() { + var v = typeof this; + return function() { + return v; + }; + } + }); + + assert.sameValue(true.toLocaleString(), "boolean"); + } + t(); + ` + + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestObjSetSym(t *testing.T) { + const SCRIPT = ` + 'use strict'; + var sym = Symbol(true); + var p1 = Object.create(null); + var p2 = Object.create(p1); + + Object.defineProperty(p1, sym, { + value: 42 + }); + + Object.defineProperty(p2, sym, { + value: 43, + writable: true, + }); + var o = Object.create(p2); + o[sym] = 44; + o[sym]; + ` + testScript(SCRIPT, intToValue(44), t) +} + +func TestObjSet(t *testing.T) { + const SCRIPT = ` + 'use strict'; + var p1 = Object.create(null); + var p2 = Object.create(p1); + + Object.defineProperty(p1, "test", { + value: 42 + }); + + Object.defineProperty(p2, "test", { + value: 43, + writable: true, + }); + var o = Object.create(p2); + o.test = 44; + o.test; + ` + testScript(SCRIPT, intToValue(44), t) +} + +func TestToValueNilValue(t *testing.T) { + r := New() + var a Value + r.Set("a", a) + ret, err := r.RunString(` + ""+a; + `) + if err != nil { + t.Fatal(err) + } + if !asciiString("null").SameAs(ret) { + t.Fatalf("ret: %v", ret) + } +} + +func TestDateConversion(t *testing.T) { + now := time.Now() + vm := New() + val, err := vm.New(vm.Get("Date").ToObject(vm), vm.ToValue(now.UnixNano()/1e6)) + if err != nil { + t.Fatal(err) + } + vm.Set("d", val) + res, err := vm.RunString(`+d`) + if err != nil { + t.Fatal(err) + } + if exp := res.Export(); exp != now.UnixNano()/1e6 { + t.Fatalf("Value does not match: %v", exp) + } + vm.Set("goval", now) + res, err = vm.RunString(`+(new Date(goval.UnixNano()/1e6))`) + if err != nil { + t.Fatal(err) + } + if exp := res.Export(); exp != now.UnixNano()/1e6 { + t.Fatalf("Value does not match: %v", exp) + } +} + +func TestNativeCtorNewTarget(t *testing.T) { + const SCRIPT = ` + function NewTarget() { + } + + var o = Reflect.construct(Number, [1], NewTarget); + o.__proto__ === NewTarget.prototype && o.toString() === "[object Number]"; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestNativeCtorNonNewCall(t *testing.T) { + vm := New() + vm.Set(`Animal`, func(call ConstructorCall) *Object { + obj := call.This + obj.Set(`name`, call.Argument(0).String()) + obj.Set(`eat`, func(call FunctionCall) Value { + self := call.This.(*Object) + return vm.ToValue(fmt.Sprintf("%s eat", self.Get(`name`))) + }) + return nil + }) + v, err := vm.RunString(` + + function __extends(d, b){ + function __() { + this.constructor = d; + } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + } + + var Cat = (function (_super) { + __extends(Cat, _super); + function Cat() { + return _super.call(this, "cat") || this; + } + return Cat; + }(Animal)); + + var cat = new Cat(); + cat instanceof Cat && cat.eat() === "cat eat"; + `) + if err != nil { + t.Fatal(err) + } + if v != valueTrue { + t.Fatal(v) + } +} + +func ExampleNewSymbol() { + sym1 := NewSymbol("66") + sym2 := NewSymbol("66") + fmt.Printf("%s %s %v", sym1, sym2, sym1.Equals(sym2)) + // Output: 66 66 false +} + +func ExampleObject_SetSymbol() { + type IterResult struct { + Done bool + Value Value + } + + vm := New() + vm.SetFieldNameMapper(UncapFieldNameMapper()) // to use IterResult + + o := vm.NewObject() + o.SetSymbol(SymIterator, func() *Object { + count := 0 + iter := vm.NewObject() + iter.Set("next", func() IterResult { + if count < 10 { + count++ + return IterResult{ + Value: vm.ToValue(count), + } + } + return IterResult{ + Done: true, + } + }) + return iter + }) + vm.Set("o", o) + + res, err := vm.RunString(` + var acc = ""; + for (var v of o) { + acc += v + " "; + } + acc; + `) + if err != nil { + panic(err) + } + fmt.Println(res) + // Output: 1 2 3 4 5 6 7 8 9 10 +} + +func ExampleRuntime_NewArray() { + vm := New() + array := vm.NewArray(1, 2, true) + vm.Set("array", array) + res, err := vm.RunString(` + var acc = ""; + for (var v of array) { + acc += v + " "; + } + acc; + `) + if err != nil { + panic(err) + } + fmt.Println(res) + // Output: 1 2 true +} + +func ExampleRuntime_SetParserOptions() { + vm := New() + vm.SetParserOptions(parser.WithDisableSourceMaps) + + res, err := vm.RunString(` + "I did not hang!"; +//# sourceMappingURL=/dev/zero`) + + if err != nil { + panic(err) + } + fmt.Println(res.String()) + // Output: I did not hang! +} + +func TestRuntime_SetParserOptions_Eval(t *testing.T) { + vm := New() + vm.SetParserOptions(parser.WithDisableSourceMaps) + + _, err := vm.RunString(` + eval("//# sourceMappingURL=/dev/zero"); + `) + if err != nil { + t.Fatal(err) + } +} + +func TestNativeCallWithRuntimeParameter(t *testing.T) { + vm := New() + vm.Set("f", func(_ FunctionCall, r *Runtime) Value { + if r == vm { + return valueTrue + } + return valueFalse + }) + ret, err := vm.RunString(`f()`) + if err != nil { + t.Fatal(err) + } + if ret != valueTrue { + t.Fatal(ret) + } +} + +func TestNestedEnumerate(t *testing.T) { + const SCRIPT = ` + var o = {baz: true, foo: true, bar: true}; + var res = ""; + for (var i in o) { + delete o.baz; + Object.defineProperty(o, "hidden", {value: true, configurable: true}); + for (var j in o) { + Object.defineProperty(o, "0", {value: true, configurable: true}); + Object.defineProperty(o, "1", {value: true, configurable: true}); + for (var k in o) {} + res += i + "-" + j + " "; + } + } + assert(compareArray(Reflect.ownKeys(o), ["0","1","foo","bar","hidden"]), "keys"); + res; + ` + testScriptWithTestLib(SCRIPT, asciiString("baz-foo baz-bar foo-foo foo-bar bar-foo bar-bar "), t) +} + +func TestAbandonedEnumerate(t *testing.T) { + const SCRIPT = ` + var o = {baz: true, foo: true, bar: true}; + var res = ""; + for (var i in o) { + delete o.baz; + for (var j in o) { + res += i + "-" + j + " "; + break; + } + } + res; + ` + testScript(SCRIPT, asciiString("baz-foo foo-foo bar-foo "), t) +} + +func TestIterCloseThrows(t *testing.T) { + const SCRIPT = ` + var returnCount = 0; + var iterable = {}; + var iterator = { + next: function() { + return { value: true }; + }, + return: function() { + returnCount += 1; + throw new Error(); + } + }; + iterable[Symbol.iterator] = function() { + return iterator; + }; + + try { + for (var i of iterable) { + break; + } + } catch (e) {}; + returnCount; + ` + testScript(SCRIPT, valueInt(1), t) +} + +func TestDeclareGlobalFunc(t *testing.T) { + const SCRIPT = ` + var initial; + + Object.defineProperty(this, 'f', { + enumerable: true, + writable: true, + configurable: false + }); + + (0,eval)('initial = f; function f() { return 2222; }'); + var desc = Object.getOwnPropertyDescriptor(this, "f"); + assert(desc.enumerable, "enumerable"); + assert(desc.writable, "writable"); + assert(!desc.configurable, "configurable"); + assert.sameValue(initial(), 2222); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestStackOverflowError(t *testing.T) { + vm := New() + vm.SetMaxCallStackSize(3) + _, err := vm.RunString(` + function f() { + f(); + } + f(); + `) + if _, ok := err.(*StackOverflowError); !ok { + t.Fatal(err) + } +} + +func TestStacktraceLocationThrowFromCatch(t *testing.T) { + vm := New() + _, err := vm.RunString(` + function main(arg) { + try { + if (arg === 1) { + return f1(); + } + if (arg === 2) { + return f2(); + } + if (arg === 3) { + return f3(); + } + } catch (e) { + throw e; + } + } + function f1() {} + function f2() { + throw new Error(); + } + function f3() {} + main(2); + `) + if err == nil { + t.Fatal("Expected error") + } + stack := err.(*Exception).stack + if len(stack) != 3 { + t.Fatalf("Unexpected stack len: %v", stack) + } + if frame := stack[0]; frame.funcName != "f2" || frame.pc != 2 { + t.Fatalf("Unexpected stack frame 0: %#v", frame) + } + if frame := stack[1]; frame.funcName != "main" || frame.pc != 17 { + t.Fatalf("Unexpected stack frame 1: %#v", frame) + } + if frame := stack[2]; frame.funcName != "" || frame.pc != 7 { + t.Fatalf("Unexpected stack frame 2: %#v", frame) + } +} + +func TestErrorStackRethrow(t *testing.T) { + const SCRIPT = ` + function f(e) { + throw e; + } + try { + f(new Error()); + } catch(e) { + assertStack(e, [["test.js", "", 6, 5]]); + } + ` + testScriptWithTestLibX(SCRIPT, _undefined, t) +} + +func TestStacktraceLocationThrowFromGo(t *testing.T) { + vm := New() + f := func() { + panic(vm.ToValue("Test")) + } + vm.Set("f", f) + _, err := vm.RunString(` + function main() { + (function noop() {})(); + return callee(); + } + function callee() { + return f(); + } + main(); + `) + if err == nil { + t.Fatal("Expected error") + } + stack := err.(*Exception).stack + if len(stack) != 4 { + t.Fatalf("Unexpected stack len: %v", stack) + } + if frame := stack[0]; !strings.HasSuffix(frame.funcName.String(), "TestStacktraceLocationThrowFromGo.func1") { + t.Fatalf("Unexpected stack frame 0: %#v", frame) + } + if frame := stack[1]; frame.funcName != "callee" || frame.pc != 2 { + t.Fatalf("Unexpected stack frame 1: %#v", frame) + } + if frame := stack[2]; frame.funcName != "main" || frame.pc != 6 { + t.Fatalf("Unexpected stack frame 2: %#v", frame) + } + if frame := stack[3]; frame.funcName != "" || frame.pc != 4 { + t.Fatalf("Unexpected stack frame 3: %#v", frame) + } +} + +func TestStacktraceLocationThrowNativeInTheMiddle(t *testing.T) { + vm := New() + v, err := vm.RunString(`(function f1() { + throw new Error("test") + })`) + if err != nil { + t.Fatal(err) + } + + var f1 func() + err = vm.ExportTo(v, &f1) + if err != nil { + t.Fatal(err) + } + + f := func() { + f1() + } + vm.Set("f", f) + _, err = vm.RunString(` + function main() { + (function noop() {})(); + return callee(); + } + function callee() { + return f(); + } + main(); + `) + if err == nil { + t.Fatal("Expected error") + } + stack := err.(*Exception).stack + if len(stack) != 5 { + t.Fatalf("Unexpected stack len: %v", stack) + } + if frame := stack[0]; frame.funcName != "f1" || frame.pc != 7 { + t.Fatalf("Unexpected stack frame 0: %#v", frame) + } + if frame := stack[1]; !strings.HasSuffix(frame.funcName.String(), "TestStacktraceLocationThrowNativeInTheMiddle.func1") { + t.Fatalf("Unexpected stack frame 1: %#v", frame) + } + if frame := stack[2]; frame.funcName != "callee" || frame.pc != 2 { + t.Fatalf("Unexpected stack frame 2: %#v", frame) + } + if frame := stack[3]; frame.funcName != "main" || frame.pc != 6 { + t.Fatalf("Unexpected stack frame 3: %#v", frame) + } + if frame := stack[4]; frame.funcName != "" || frame.pc != 4 { + t.Fatalf("Unexpected stack frame 4: %#v", frame) + } +} + +func TestStrToInt64(t *testing.T) { + if _, ok := strToInt64(""); ok { + t.Fatal("") + } + if n, ok := strToInt64("0"); !ok || n != 0 { + t.Fatal("0", n, ok) + } + if n, ok := strToInt64("-0"); ok { + t.Fatal("-0", n, ok) + } + if n, ok := strToInt64("-1"); !ok || n != -1 { + t.Fatal("-1", n, ok) + } + if n, ok := strToInt64("9223372036854775808"); ok { + t.Fatal("max+1", n, ok) + } + if n, ok := strToInt64("9223372036854775817"); ok { + t.Fatal("9223372036854775817", n, ok) + } + if n, ok := strToInt64("-9223372036854775818"); ok { + t.Fatal("-9223372036854775818", n, ok) + } + if n, ok := strToInt64("9223372036854775807"); !ok || n != 9223372036854775807 { + t.Fatal("max", n, ok) + } + if n, ok := strToInt64("-9223372036854775809"); ok { + t.Fatal("min-1", n, ok) + } + if n, ok := strToInt64("-9223372036854775808"); !ok || n != -9223372036854775808 { + t.Fatal("min", n, ok) + } + if n, ok := strToInt64("-00"); ok { + t.Fatal("-00", n, ok) + } + if n, ok := strToInt64("-01"); ok { + t.Fatal("-01", n, ok) + } +} + +func TestStrToInt32(t *testing.T) { + if _, ok := strToInt32(""); ok { + t.Fatal("") + } + if n, ok := strToInt32("0"); !ok || n != 0 { + t.Fatal("0", n, ok) + } + if n, ok := strToInt32("-0"); ok { + t.Fatal("-0", n, ok) + } + if n, ok := strToInt32("-1"); !ok || n != -1 { + t.Fatal("-1", n, ok) + } + if n, ok := strToInt32("2147483648"); ok { + t.Fatal("max+1", n, ok) + } + if n, ok := strToInt32("2147483657"); ok { + t.Fatal("2147483657", n, ok) + } + if n, ok := strToInt32("-2147483658"); ok { + t.Fatal("-2147483658", n, ok) + } + if n, ok := strToInt32("2147483647"); !ok || n != 2147483647 { + t.Fatal("max", n, ok) + } + if n, ok := strToInt32("-2147483649"); ok { + t.Fatal("min-1", n, ok) + } + if n, ok := strToInt32("-2147483648"); !ok || n != -2147483648 { + t.Fatal("min", n, ok) + } + if n, ok := strToInt32("-00"); ok { + t.Fatal("-00", n, ok) + } + if n, ok := strToInt32("-01"); ok { + t.Fatal("-01", n, ok) + } +} + +func TestDestructSymbol(t *testing.T) { + const SCRIPT = ` + var S = Symbol("S"); + var s, rest; + + ({[S]: s, ...rest} = {[S]: true, test: 1}); + assert.sameValue(s, true, "S"); + assert(deepEqual(rest, {test: 1}), "rest"); + ` + testScriptWithTestLibX(SCRIPT, _undefined, t) +} + +func TestAccessorFuncName(t *testing.T) { + const SCRIPT = ` + const namedSym = Symbol('test262'); + const emptyStrSym = Symbol(""); + const anonSym = Symbol(); + + const o = { + get id() {}, + get [anonSym]() {}, + get [namedSym]() {}, + get [emptyStrSym]() {}, + set id(v) {}, + set [anonSym](v) {}, + set [namedSym](v) {}, + set [emptyStrSym](v) {} + }; + + let prop; + prop = Object.getOwnPropertyDescriptor(o, 'id'); + assert.sameValue(prop.get.name, 'get id'); + assert.sameValue(prop.set.name, 'set id'); + + prop = Object.getOwnPropertyDescriptor(o, anonSym); + assert.sameValue(prop.get.name, 'get '); + assert.sameValue(prop.set.name, 'set '); + + prop = Object.getOwnPropertyDescriptor(o, emptyStrSym); + assert.sameValue(prop.get.name, 'get []'); + assert.sameValue(prop.set.name, 'set []'); + + prop = Object.getOwnPropertyDescriptor(o, namedSym); + assert.sameValue(prop.get.name, 'get [test262]'); + assert.sameValue(prop.set.name, 'set [test262]'); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestCoverFuncName(t *testing.T) { + const SCRIPT = ` + var namedSym = Symbol(''); + var anonSym = Symbol(); + var o; + + o = { + xId: (0, function() {}), + id: (function() {}), + id1: function x() {}, + [anonSym]: (function() {}), + [namedSym]: (function() {}) + }; + + assert(o.xId.name !== 'xId'); + assert.sameValue(o.id1.name, 'x'); + assert.sameValue(o.id.name, 'id', 'via IdentifierName'); + assert.sameValue(o[anonSym].name, '', 'via anonymous Symbol'); + assert.sameValue(o[namedSym].name, '[]', 'via Symbol'); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestAnonFuncName(t *testing.T) { + const SCRIPT = ` + const d = Object.getOwnPropertyDescriptor((function() {}), 'name'); + d !== undefined && d.value === ''; + ` + testScript(SCRIPT, valueTrue, t) +} + +func TestStringToBytesConversion(t *testing.T) { + vm := New() + v := vm.ToValue("Test") + var b []byte + err := vm.ExportTo(v, &b) + if err != nil { + t.Fatal(err) + } + if string(b) != "Test" { + t.Fatal(b) + } +} + +func TestPromiseAll(t *testing.T) { + const SCRIPT = ` +var p1 = new Promise(function() {}); +var p2 = new Promise(function() {}); +var p3 = new Promise(function() {}); +var callCount = 0; +var currentThis = p1; +var nextThis = p2; +var afterNextThis = p3; + +p1.then = p2.then = p3.then = function(a, b) { + assert.sameValue(typeof a, 'function', 'type of first argument'); + assert.sameValue( + a.length, + 1, + 'ES6 25.4.1.3.2: The length property of a promise resolve function is 1.' + ); + assert.sameValue(typeof b, 'function', 'type of second argument'); + assert.sameValue( + b.length, + 1, + 'ES6 25.4.1.3.1: The length property of a promise reject function is 1.' + ); + assert.sameValue(arguments.length, 2, '"then"" invoked with two arguments'); + assert.sameValue(this, currentThis, '"this" value'); + + currentThis = nextThis; + nextThis = afterNextThis; + afterNextThis = null; + + callCount += 1; +}; + +Promise.all([p1, p2, p3]); + +assert.sameValue(callCount, 3, '"then"" invoked once for every iterated value'); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestPromiseExport(t *testing.T) { + vm := New() + p, _, _ := vm.NewPromise() + pv := vm.ToValue(p) + if actual := pv.ExportType(); actual != reflect.TypeOf((*Promise)(nil)) { + t.Fatalf("Export type: %v", actual) + } + + if ev := pv.Export(); ev != p { + t.Fatalf("Export value: %v", ev) + } +} + +func TestErrorStack(t *testing.T) { + const SCRIPT = ` + const err = new Error("test"); + if (!("stack" in err)) { + throw new Error("in"); + } + if (Reflect.ownKeys(err)[0] !== "stack") { + throw new Error("property order"); + } + const stack = err.stack; + if (stack !== "Error: test\n\tat test.js:2:14(3)\n") { + throw new Error(stack); + } + delete err.stack; + if ("stack" in err) { + throw new Error("stack still in err after delete"); + } + ` + testScript(SCRIPT, _undefined, t) +} + +func TestErrorFormatSymbols(t *testing.T) { + vm := New() + vm.Set("a", func() (Value, error) { return nil, errors.New("something %s %f") }) + _, err := vm.RunString("a()") + if !strings.Contains(err.Error(), "something %s %f") { + t.Fatalf("Wrong value %q", err.Error()) + } +} + +func TestPanicPassthrough(t *testing.T) { + const panicString = "Test panic" + r := New() + r.Set("f", func() { + panic(panicString) + }) + defer func() { + if x := recover(); x != nil { + if x != panicString { + t.Fatalf("Wrong panic value: %v", x) + } + if len(r.vm.callStack) > 0 { + t.Fatal("vm.callStack is not empty") + } + } else { + t.Fatal("No panic") + } + }() + _, _ = r.RunString("f()") + t.Fatal("Should not reach here") +} + +func TestSuspendResumeRelStackLen(t *testing.T) { + const SCRIPT = ` + async function f2() { + throw new Error("test"); + } + + async function f1() { + let a = [1]; + for (let i of a) { + try { + await f2(); + } catch { + return true; + } + } + } + + async function f() { + let a = [1]; + for (let i of a) { + return await f1(); + } + } + return f(); + ` + testAsyncFunc(SCRIPT, valueTrue, t) +} + +func TestSuspendResumeStacks(t *testing.T) { + const SCRIPT = ` +async function f1() { + throw new Error(); +} +async function f() { + try { + await f1(); + } catch {} +} + +result = await f(); + ` + testAsyncFunc(SCRIPT, _undefined, t) +} + +func TestNestedTopLevelConstructorCall(t *testing.T) { + r := New() + c := func(call ConstructorCall, rt *Runtime) *Object { + if _, err := rt.RunString("(5)"); err != nil { + panic(err) + } + return nil + } + if err := r.Set("C", c); err != nil { + panic(err) + } + if _, err := r.RunString("new C()"); err != nil { + panic(err) + } +} + +func TestNestedTopLevelConstructorPanicAsync(t *testing.T) { + r := New() + c := func(call ConstructorCall, rt *Runtime) *Object { + c, ok := AssertFunction(rt.ToValue(func() {})) + if !ok { + panic("wat") + } + if _, err := c(Undefined()); err != nil { + panic(err) + } + return nil + } + if err := r.Set("C", c); err != nil { + panic(err) + } + if _, err := r.RunString("new C()"); err != nil { + panic(err) + } +} + +func TestAsyncFuncThrow(t *testing.T) { + const SCRIPT = ` + class TestError extends Error { + } + + async function f() { + throw new TestError(); + } + + async function f1() { + try { + await f(); + } catch (e) { + assert.sameValue(e.constructor.name, TestError.name); + return; + } + throw new Error("No exception was thrown"); + } + await f1(); + return undefined; + ` + testAsyncFuncWithTestLib(SCRIPT, _undefined, t) +} + +func TestAsyncStacktrace(t *testing.T) { + // Do not reformat, assertions depend on the line and column numbers + const SCRIPT = ` + let ex; + async function foo(x) { + await bar(x); + } + + async function bar(x) { + await x; + throw new Error("Let's have a look..."); + } + + try { + await foo(1); + } catch (e) { + assertStack(e, [ + ["test.js", "bar", 9, 10], + ["test.js", "foo", 4, 13], + ["test.js", "test", 13, 12], + ]); + } + ` + testAsyncFuncWithTestLibX(SCRIPT, _undefined, t) +} + +func TestPanicPropagation(t *testing.T) { + r := New() + r.Set("doPanic", func() { + panic(true) + }) + v, err := r.RunString(`(function() { + doPanic(); + })`) + if err != nil { + t.Fatal(err) + } + f, ok := AssertFunction(v) + if !ok { + t.Fatal("not a function") + } + defer func() { + if x := recover(); x != nil { + if x != true { + t.Fatal("Invalid panic value") + } + } + }() + _, _ = f(nil) + t.Fatal("Expected panic") +} + +func TestAwaitInParameters(t *testing.T) { + _, err := Compile("", ` + async function g() { + async function inner(a = 1 + await 1) { + } + } + `, false) + if err == nil { + t.Fatal("Expected error") + } +} + +func ExampleRuntime_ForOf() { + r := New() + v, err := r.RunString(` + new Map().set("a", 1).set("b", 2); + `) + if err != nil { + panic(err) + } + + var sb strings.Builder + ex := r.Try(func() { + r.ForOf(v, func(v Value) bool { + o := v.ToObject(r) + key := o.Get("0") + value := o.Get("1") + + sb.WriteString(key.String()) + sb.WriteString("=") + sb.WriteString(value.String()) + sb.WriteString(",") + + return true + }) + }) + if ex != nil { + panic(ex) + } + fmt.Println(sb.String()) + // Output: a=1,b=2, +} + +func TestDestructAssignToSymbol(t *testing.T) { + const SCRIPT = ` + const s = Symbol('s'); + const target = {}; + + ({a: target[s]} = {a: 42}); + assert.sameValue(target[s], 42); +` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestToNumber(t *testing.T) { + const SCRIPT = ` + assert(isNaN(Number("+"))); + assert(isNaN(Number("++"))); + assert(isNaN(Number("-"))); + assert(isNaN(Number("0xfp1"))); + assert(isNaN(Number("0Xfp1"))); + assert(isNaN(Number("+0xfp1"))); + assert(isNaN(Number(" +0xfp1"))); + assert(isNaN(Number(" + 0xfp1"))); + assert(isNaN(Number(" 0xfp1"))); + assert(isNaN(Number("-0xfp1"))); + assert(isNaN(Number("- 0xfp1"))); + assert(isNaN(Number(" - 0xfp1"))); + assert.sameValue(Number("0."), 0); + assert.sameValue(Number(" "), 0); + assert.sameValue(Number(" Infinity"), Infinity); + + let a = [1]; + assert.sameValue(1, a.at("0xfp1")); + assert.sameValue(1, a.at(" 0xfp1")); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +/* +func TestArrayConcatSparse(t *testing.T) { +function foo(a,b,c) + { + arguments[0] = 1; arguments[1] = 'str'; arguments[2] = 2.1; + if(1 === a && 'str' === b && 2.1 === c) + return true; + } + + + const SCRIPT = ` + var a1 = []; + var a2 = []; + a1[500000] = 1; + a2[1000000] = 2; + var a3 = a1.concat(a2); + a3.length === 1500002 && a3[500000] === 1 && a3[1500001] == 2; + ` + + testScript(SCRIPT, valueTrue, t) +} +*/ + +func BenchmarkCallReflect(b *testing.B) { + vm := New() + vm.Set("f", func(v Value) { + + }) + + prg := MustCompile("test.js", "f(null)", true) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + vm.RunProgram(prg) + } +} + +func BenchmarkCallNative(b *testing.B) { + vm := New() + vm.Set("f", func(call FunctionCall) (ret Value) { + return + }) + + prg := MustCompile("test.js", "f(null)", true) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + vm.RunProgram(prg) + } +} + +func BenchmarkCallJS(b *testing.B) { + vm := New() + _, err := vm.RunString(` + function f() { + return 42; + } + `) + + if err != nil { + b.Fatal(err) + } + + prg := MustCompile("test.js", "f(null)", true) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + vm.RunProgram(prg) + } +} + +func BenchmarkMainLoop(b *testing.B) { + vm := New() + + const SCRIPT = ` + for (var i=0; i<100000; i++) { + } + ` + + prg := MustCompile("test.js", SCRIPT, true) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + vm.RunProgram(prg) + } +} + +func BenchmarkStringMapGet(b *testing.B) { + m := make(map[string]Value) + for i := 0; i < 100; i++ { + m[strconv.Itoa(i)] = intToValue(int64(i)) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + if m["50"] == nil { + b.Fatal() + } + } +} + +func BenchmarkValueStringMapGet(b *testing.B) { + m := make(map[String]Value) + for i := 0; i < 100; i++ { + m[asciiString(strconv.Itoa(i))] = intToValue(int64(i)) + } + b.ResetTimer() + var key String = asciiString("50") + for i := 0; i < b.N; i++ { + if m[key] == nil { + b.Fatal() + } + } +} + +func BenchmarkAsciiStringMapGet(b *testing.B) { + m := make(map[asciiString]Value) + for i := 0; i < 100; i++ { + m[asciiString(strconv.Itoa(i))] = intToValue(int64(i)) + } + b.ResetTimer() + var key = asciiString("50") + for i := 0; i < b.N; i++ { + if m[key] == nil { + b.Fatal() + } + } +} + +func BenchmarkNew(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + New() + } +} diff --git a/goja/staticcheck.conf b/goja/staticcheck.conf new file mode 100644 index 0000000..2441b9d --- /dev/null +++ b/goja/staticcheck.conf @@ -0,0 +1 @@ +checks = ["all", "-ST1000", "-ST1003", "-ST1005", "-ST1006", "-ST1012", "-ST1021", "-ST1020", "-ST1008"] diff --git a/goja/string.go b/goja/string.go new file mode 100644 index 0000000..0eaf3ef --- /dev/null +++ b/goja/string.go @@ -0,0 +1,364 @@ +package goja + +import ( + "io" + "strconv" + "strings" + "unicode/utf8" + + "github.com/dop251/goja/unistring" +) + +const ( + __proto__ = "__proto__" +) + +var ( + stringTrue String = asciiString("true") + stringFalse String = asciiString("false") + stringNull String = asciiString("null") + stringUndefined String = asciiString("undefined") + stringObjectC String = asciiString("object") + stringFunction String = asciiString("function") + stringBoolean String = asciiString("boolean") + stringString String = asciiString("string") + stringSymbol String = asciiString("symbol") + stringNumber String = asciiString("number") + stringBigInt String = asciiString("bigint") + stringNaN String = asciiString("NaN") + stringInfinity = asciiString("Infinity") + stringNegInfinity = asciiString("-Infinity") + stringBound_ String = asciiString("bound ") + stringEmpty String = asciiString("") + + stringError String = asciiString("Error") + stringAggregateError String = asciiString("AggregateError") + stringTypeError String = asciiString("TypeError") + stringReferenceError String = asciiString("ReferenceError") + stringSyntaxError String = asciiString("SyntaxError") + stringRangeError String = asciiString("RangeError") + stringEvalError String = asciiString("EvalError") + stringURIError String = asciiString("URIError") + stringGoError String = asciiString("GoError") + + stringObjectNull String = asciiString("[object Null]") + stringObjectUndefined String = asciiString("[object Undefined]") + stringInvalidDate String = asciiString("Invalid Date") +) + +type utf16Reader interface { + readChar() (c uint16, err error) +} + +// String represents an ECMAScript string Value. Its internal representation depends on the contents of the +// string, but in any case it is capable of holding any UTF-16 string, either valid or invalid. +// Instances of this type, as any other primitive values, are goroutine-safe and can be passed between runtimes. +// Strings can be created using Runtime.ToValue(goString) or StringFromUTF16. +type String interface { + Value + CharAt(int) uint16 + Length() int + Concat(String) String + Substring(start, end int) String + CompareTo(String) int + Reader() io.RuneReader + utf16Reader() utf16Reader + utf16RuneReader() io.RuneReader + utf16Runes() []rune + index(String, int) int + lastIndex(String, int) int + toLower() String + toUpper() String + toTrimmedUTF8() string +} + +type stringIterObject struct { + baseObject + reader io.RuneReader +} + +func isUTF16FirstSurrogate(c uint16) bool { + return c >= 0xD800 && c <= 0xDBFF +} + +func isUTF16SecondSurrogate(c uint16) bool { + return c >= 0xDC00 && c <= 0xDFFF +} + +func (si *stringIterObject) next() Value { + if si.reader == nil { + return si.val.runtime.createIterResultObject(_undefined, true) + } + r, _, err := si.reader.ReadRune() + if err == io.EOF { + si.reader = nil + return si.val.runtime.createIterResultObject(_undefined, true) + } + return si.val.runtime.createIterResultObject(stringFromRune(r), false) +} + +func stringFromRune(r rune) String { + if r < utf8.RuneSelf { + var sb strings.Builder + sb.WriteByte(byte(r)) + return asciiString(sb.String()) + } + var sb unicodeStringBuilder + sb.WriteRune(r) + return sb.String() +} + +func (r *Runtime) createStringIterator(s String) Value { + o := &Object{runtime: r} + + si := &stringIterObject{ + reader: &lenientUtf16Decoder{utf16Reader: s.utf16Reader()}, + } + si.class = classObject + si.val = o + si.extensible = true + o.self = si + si.prototype = r.getStringIteratorPrototype() + si.init() + + return o +} + +type stringObject struct { + baseObject + value String + length int + lengthProp valueProperty +} + +func newStringValue(s string) String { + if u := unistring.Scan(s); u != nil { + return unicodeString(u) + } + return asciiString(s) +} + +func stringValueFromRaw(raw unistring.String) String { + if b := raw.AsUtf16(); b != nil { + return unicodeString(b) + } + return asciiString(raw) +} + +func (s *stringObject) init() { + s.baseObject.init() + s.setLength() +} + +func (s *stringObject) setLength() { + if s.value != nil { + s.length = s.value.Length() + } + s.lengthProp.value = intToValue(int64(s.length)) + s._put("length", &s.lengthProp) +} + +func (s *stringObject) getStr(name unistring.String, receiver Value) Value { + if i := strToGoIdx(name); i >= 0 && i < s.length { + return s._getIdx(i) + } + return s.baseObject.getStr(name, receiver) +} + +func (s *stringObject) getIdx(idx valueInt, receiver Value) Value { + i := int(idx) + if i >= 0 && i < s.length { + return s._getIdx(i) + } + return s.baseObject.getStr(idx.string(), receiver) +} + +func (s *stringObject) getOwnPropStr(name unistring.String) Value { + if i := strToGoIdx(name); i >= 0 && i < s.length { + val := s._getIdx(i) + return &valueProperty{ + value: val, + enumerable: true, + } + } + + return s.baseObject.getOwnPropStr(name) +} + +func (s *stringObject) getOwnPropIdx(idx valueInt) Value { + i := int64(idx) + if i >= 0 { + if i < int64(s.length) { + val := s._getIdx(int(i)) + return &valueProperty{ + value: val, + enumerable: true, + } + } + return nil + } + + return s.baseObject.getOwnPropStr(idx.string()) +} + +func (s *stringObject) _getIdx(idx int) Value { + return s.value.Substring(idx, idx+1) +} + +func (s *stringObject) setOwnStr(name unistring.String, val Value, throw bool) bool { + if i := strToGoIdx(name); i >= 0 && i < s.length { + s.val.runtime.typeErrorResult(throw, "Cannot assign to read only property '%d' of a String", i) + return false + } + + return s.baseObject.setOwnStr(name, val, throw) +} + +func (s *stringObject) setOwnIdx(idx valueInt, val Value, throw bool) bool { + i := int64(idx) + if i >= 0 && i < int64(s.length) { + s.val.runtime.typeErrorResult(throw, "Cannot assign to read only property '%d' of a String", i) + return false + } + + return s.baseObject.setOwnStr(idx.string(), val, throw) +} + +func (s *stringObject) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { + return s._setForeignStr(name, s.getOwnPropStr(name), val, receiver, throw) +} + +func (s *stringObject) setForeignIdx(idx valueInt, val, receiver Value, throw bool) (bool, bool) { + return s._setForeignIdx(idx, s.getOwnPropIdx(idx), val, receiver, throw) +} + +func (s *stringObject) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + if i := strToGoIdx(name); i >= 0 && i < s.length { + _, ok := s._defineOwnProperty(name, &valueProperty{enumerable: true}, descr, throw) + return ok + } + + return s.baseObject.defineOwnPropertyStr(name, descr, throw) +} + +func (s *stringObject) defineOwnPropertyIdx(idx valueInt, descr PropertyDescriptor, throw bool) bool { + i := int64(idx) + if i >= 0 && i < int64(s.length) { + s.val.runtime.typeErrorResult(throw, "Cannot redefine property: %d", i) + return false + } + + return s.baseObject.defineOwnPropertyStr(idx.string(), descr, throw) +} + +type stringPropIter struct { + str String // separate, because obj can be the singleton + obj *stringObject + idx, length int +} + +func (i *stringPropIter) next() (propIterItem, iterNextFunc) { + if i.idx < i.length { + name := strconv.Itoa(i.idx) + i.idx++ + return propIterItem{name: asciiString(name), enumerable: _ENUM_TRUE}, i.next + } + + return i.obj.baseObject.iterateStringKeys()() +} + +func (s *stringObject) iterateStringKeys() iterNextFunc { + return (&stringPropIter{ + str: s.value, + obj: s, + length: s.length, + }).next +} + +func (s *stringObject) stringKeys(all bool, accum []Value) []Value { + for i := 0; i < s.length; i++ { + accum = append(accum, asciiString(strconv.Itoa(i))) + } + + return s.baseObject.stringKeys(all, accum) +} + +func (s *stringObject) deleteStr(name unistring.String, throw bool) bool { + if i := strToGoIdx(name); i >= 0 && i < s.length { + s.val.runtime.typeErrorResult(throw, "Cannot delete property '%d' of a String", i) + return false + } + + return s.baseObject.deleteStr(name, throw) +} + +func (s *stringObject) deleteIdx(idx valueInt, throw bool) bool { + i := int64(idx) + if i >= 0 && i < int64(s.length) { + s.val.runtime.typeErrorResult(throw, "Cannot delete property '%d' of a String", i) + return false + } + + return s.baseObject.deleteStr(idx.string(), throw) +} + +func (s *stringObject) hasOwnPropertyStr(name unistring.String) bool { + if i := strToGoIdx(name); i >= 0 && i < s.length { + return true + } + return s.baseObject.hasOwnPropertyStr(name) +} + +func (s *stringObject) hasOwnPropertyIdx(idx valueInt) bool { + i := int64(idx) + if i >= 0 && i < int64(s.length) { + return true + } + return s.baseObject.hasOwnPropertyStr(idx.string()) +} + +func devirtualizeString(s String) (asciiString, unicodeString) { + switch s := s.(type) { + case asciiString: + return s, nil + case unicodeString: + return "", s + case *importedString: + s.ensureScanned() + if s.u != nil { + return "", s.u + } + return asciiString(s.s), nil + default: + panic(unknownStringTypeErr(s)) + } +} + +func unknownStringTypeErr(v Value) interface{} { + return newTypeError("Internal bug: unknown string type: %T", v) +} + +// StringFromUTF16 creates a string value from an array of UTF-16 code units. The result is a copy, so the initial +// slice can be modified after calling this function (but it must not be modified while the function is running). +// No validation of any kind is performed. +func StringFromUTF16(chars []uint16) String { + isAscii := true + for _, c := range chars { + if c >= utf8.RuneSelf { + isAscii = false + break + } + } + if isAscii { + var sb strings.Builder + sb.Grow(len(chars)) + for _, c := range chars { + sb.WriteByte(byte(c)) + } + return asciiString(sb.String()) + } + buf := make([]uint16, len(chars)+1) + buf[0] = unistring.BOM + copy(buf[1:], chars) + return unicodeString(buf) +} diff --git a/goja/string_ascii.go b/goja/string_ascii.go new file mode 100644 index 0000000..6b13784 --- /dev/null +++ b/goja/string_ascii.go @@ -0,0 +1,401 @@ +package goja + +import ( + "hash/maphash" + "io" + "math" + "math/big" + "reflect" + "strconv" + "strings" + + "github.com/dop251/goja/unistring" +) + +type asciiString string + +type asciiRuneReader struct { + s asciiString + pos int +} + +func (rr *asciiRuneReader) ReadRune() (r rune, size int, err error) { + if rr.pos < len(rr.s) { + r = rune(rr.s[rr.pos]) + size = 1 + rr.pos++ + } else { + err = io.EOF + } + return +} + +type asciiUtf16Reader struct { + s asciiString + pos int +} + +func (rr *asciiUtf16Reader) readChar() (c uint16, err error) { + if rr.pos < len(rr.s) { + c = uint16(rr.s[rr.pos]) + rr.pos++ + } else { + err = io.EOF + } + return +} + +func (rr *asciiUtf16Reader) ReadRune() (r rune, size int, err error) { + if rr.pos < len(rr.s) { + r = rune(rr.s[rr.pos]) + rr.pos++ + size = 1 + } else { + err = io.EOF + } + return +} + +func (s asciiString) Reader() io.RuneReader { + return &asciiRuneReader{ + s: s, + } +} + +func (s asciiString) utf16Reader() utf16Reader { + return &asciiUtf16Reader{ + s: s, + } +} + +func (s asciiString) utf16RuneReader() io.RuneReader { + return &asciiUtf16Reader{ + s: s, + } +} + +func (s asciiString) utf16Runes() []rune { + runes := make([]rune, len(s)) + for i := 0; i < len(s); i++ { + runes[i] = rune(s[i]) + } + return runes +} + +// ss must be trimmed +func stringToInt(ss string) (int64, error) { + if ss == "" { + return 0, nil + } + if ss == "-0" { + return 0, strconv.ErrSyntax + } + if len(ss) > 2 { + switch ss[:2] { + case "0x", "0X": + return strconv.ParseInt(ss[2:], 16, 64) + case "0b", "0B": + return strconv.ParseInt(ss[2:], 2, 64) + case "0o", "0O": + return strconv.ParseInt(ss[2:], 8, 64) + } + } + return strconv.ParseInt(ss, 10, 64) +} + +func (s asciiString) _toInt(trimmed string) (int64, error) { + return stringToInt(trimmed) +} + +func isRangeErr(err error) bool { + if err, ok := err.(*strconv.NumError); ok { + return err.Err == strconv.ErrRange + } + return false +} + +func (s asciiString) _toFloat(trimmed string) (float64, error) { + if trimmed == "" { + return 0, nil + } + if trimmed == "-0" { + var f float64 + return -f, nil + } + + // Go allows underscores in numbers, when parsed as floats, but ECMAScript expect them to be interpreted as NaN. + if strings.ContainsRune(trimmed, '_') { + return 0, strconv.ErrSyntax + } + + // Hexadecimal floats are not supported by ECMAScript. + if len(trimmed) >= 2 { + var prefix string + if trimmed[0] == '-' || trimmed[0] == '+' { + prefix = trimmed[1:] + } else { + prefix = trimmed + } + if len(prefix) >= 2 && prefix[0] == '0' && (prefix[1] == 'x' || prefix[1] == 'X') { + return 0, strconv.ErrSyntax + } + } + + f, err := strconv.ParseFloat(trimmed, 64) + if err == nil && math.IsInf(f, 0) { + ss := strings.ToLower(trimmed) + if strings.HasPrefix(ss, "inf") || strings.HasPrefix(ss, "-inf") || strings.HasPrefix(ss, "+inf") { + // We handle "Infinity" separately, prevent from being parsed as Infinity due to strconv.ParseFloat() permissive syntax + return 0, strconv.ErrSyntax + } + } + if isRangeErr(err) { + err = nil + } + return f, err +} + +func (s asciiString) ToInteger() int64 { + ss := strings.TrimSpace(string(s)) + if ss == "" { + return 0 + } + if ss == "Infinity" || ss == "+Infinity" { + return math.MaxInt64 + } + if ss == "-Infinity" { + return math.MinInt64 + } + i, err := s._toInt(ss) + if err != nil { + f, err := s._toFloat(ss) + if err == nil { + return int64(f) + } + } + return i +} + +func (s asciiString) toString() String { + return s +} + +func (s asciiString) ToString() Value { + return s +} + +func (s asciiString) String() string { + return string(s) +} + +func (s asciiString) ToFloat() float64 { + ss := strings.TrimSpace(string(s)) + if ss == "" { + return 0 + } + if ss == "Infinity" || ss == "+Infinity" { + return math.Inf(1) + } + if ss == "-Infinity" { + return math.Inf(-1) + } + f, err := s._toFloat(ss) + if err != nil { + i, err := s._toInt(ss) + if err == nil { + return float64(i) + } + f = math.NaN() + } + return f +} + +func (s asciiString) ToBoolean() bool { + return s != "" +} + +func (s asciiString) ToNumber() Value { + ss := strings.TrimSpace(string(s)) + if ss == "" { + return intToValue(0) + } + if ss == "Infinity" || ss == "+Infinity" { + return _positiveInf + } + if ss == "-Infinity" { + return _negativeInf + } + + if i, err := s._toInt(ss); err == nil { + return intToValue(i) + } + + if f, err := s._toFloat(ss); err == nil { + return floatToValue(f) + } + + return _NaN +} + +func (s asciiString) ToObject(r *Runtime) *Object { + return r._newString(s, r.getStringPrototype()) +} + +func (s asciiString) SameAs(other Value) bool { + return s.StrictEquals(other) +} + +func (s asciiString) Equals(other Value) bool { + if s.StrictEquals(other) { + return true + } + + if o, ok := other.(valueInt); ok { + if o1, e := s._toInt(strings.TrimSpace(string(s))); e == nil { + return o1 == int64(o) + } + return false + } + + if o, ok := other.(valueFloat); ok { + return s.ToFloat() == float64(o) + } + + if o, ok := other.(valueBool); ok { + if o1, e := s._toFloat(strings.TrimSpace(string(s))); e == nil { + return o1 == o.ToFloat() + } + return false + } + + if o, ok := other.(*valueBigInt); ok { + bigInt, err := stringToBigInt(s.toTrimmedUTF8()) + if err != nil { + return false + } + return bigInt.Cmp((*big.Int)(o)) == 0 + } + + if o, ok := other.(*Object); ok { + return s.Equals(o.toPrimitive()) + } + return false +} + +func (s asciiString) StrictEquals(other Value) bool { + if otherStr, ok := other.(asciiString); ok { + return s == otherStr + } + if otherStr, ok := other.(*importedString); ok { + if otherStr.u == nil { + return string(s) == otherStr.s + } + } + return false +} + +func (s asciiString) baseObject(r *Runtime) *Object { + ss := r.getStringSingleton() + ss.value = s + ss.setLength() + return ss.val +} + +func (s asciiString) hash(hash *maphash.Hash) uint64 { + _, _ = hash.WriteString(string(s)) + h := hash.Sum64() + hash.Reset() + return h +} + +func (s asciiString) CharAt(idx int) uint16 { + return uint16(s[idx]) +} + +func (s asciiString) Length() int { + return len(s) +} + +func (s asciiString) Concat(other String) String { + a, u := devirtualizeString(other) + if u != nil { + b := make([]uint16, len(s)+len(u)) + b[0] = unistring.BOM + for i := 0; i < len(s); i++ { + b[i+1] = uint16(s[i]) + } + copy(b[len(s)+1:], u[1:]) + return unicodeString(b) + } + return s + a +} + +func (s asciiString) Substring(start, end int) String { + return s[start:end] +} + +func (s asciiString) CompareTo(other String) int { + switch other := other.(type) { + case asciiString: + return strings.Compare(string(s), string(other)) + case unicodeString: + return strings.Compare(string(s), other.String()) + case *importedString: + return strings.Compare(string(s), other.s) + default: + panic(newTypeError("Internal bug: unknown string type: %T", other)) + } +} + +func (s asciiString) index(substr String, start int) int { + a, u := devirtualizeString(substr) + if u == nil { + if start > len(s) { + return -1 + } + p := strings.Index(string(s[start:]), string(a)) + if p >= 0 { + return p + start + } + } + return -1 +} + +func (s asciiString) lastIndex(substr String, pos int) int { + a, u := devirtualizeString(substr) + if u == nil { + end := pos + len(a) + var ss string + if end > len(s) { + ss = string(s) + } else { + ss = string(s[:end]) + } + return strings.LastIndex(ss, string(a)) + } + return -1 +} + +func (s asciiString) toLower() String { + return asciiString(strings.ToLower(string(s))) +} + +func (s asciiString) toUpper() String { + return asciiString(strings.ToUpper(string(s))) +} + +func (s asciiString) toTrimmedUTF8() string { + return strings.TrimSpace(string(s)) +} + +func (s asciiString) string() unistring.String { + return unistring.String(s) +} + +func (s asciiString) Export() interface{} { + return string(s) +} + +func (s asciiString) ExportType() reflect.Type { + return reflectTypeString +} diff --git a/goja/string_imported.go b/goja/string_imported.go new file mode 100644 index 0000000..1c6cae8 --- /dev/null +++ b/goja/string_imported.go @@ -0,0 +1,307 @@ +package goja + +import ( + "hash/maphash" + "io" + "math" + "reflect" + "strings" + "unicode/utf16" + "unicode/utf8" + + "github.com/dop251/goja/parser" + "github.com/dop251/goja/unistring" + + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +// Represents a string imported from Go. The idea is to delay the scanning for unicode characters and converting +// to unicodeString until necessary. This way strings that are merely passed through never get scanned which +// saves CPU and memory. +// Currently, importedString is created in 2 cases: Runtime.ToValue() for strings longer than 16 bytes and as a result +// of JSON.stringify() if it may contain unicode characters. More cases could be added in the future. +type importedString struct { + s string + u unicodeString + + scanned bool +} + +func (i *importedString) scan() { + i.u = unistring.Scan(i.s) + i.scanned = true +} + +func (i *importedString) ensureScanned() { + if !i.scanned { + i.scan() + } +} + +func (i *importedString) ToInteger() int64 { + i.ensureScanned() + if i.u != nil { + return 0 + } + return asciiString(i.s).ToInteger() +} + +func (i *importedString) toString() String { + return i +} + +func (i *importedString) string() unistring.String { + i.ensureScanned() + if i.u != nil { + return unistring.FromUtf16(i.u) + } + return unistring.String(i.s) +} + +func (i *importedString) ToString() Value { + return i +} + +func (i *importedString) String() string { + return i.s +} + +func (i *importedString) ToFloat() float64 { + i.ensureScanned() + if i.u != nil { + return math.NaN() + } + return asciiString(i.s).ToFloat() +} + +func (i *importedString) ToNumber() Value { + i.ensureScanned() + if i.u != nil { + return i.u.ToNumber() + } + return asciiString(i.s).ToNumber() +} + +func (i *importedString) ToBoolean() bool { + return len(i.s) != 0 +} + +func (i *importedString) ToObject(r *Runtime) *Object { + return r._newString(i, r.getStringPrototype()) +} + +func (i *importedString) SameAs(other Value) bool { + return i.StrictEquals(other) +} + +func (i *importedString) Equals(other Value) bool { + if i.StrictEquals(other) { + return true + } + i.ensureScanned() + if i.u != nil { + return i.u.Equals(other) + } + return asciiString(i.s).Equals(other) +} + +func (i *importedString) StrictEquals(other Value) bool { + switch otherStr := other.(type) { + case asciiString: + if i.u != nil { + return false + } + return i.s == string(otherStr) + case unicodeString: + i.ensureScanned() + if i.u != nil && i.u.equals(otherStr) { + return true + } + case *importedString: + return i.s == otherStr.s + } + return false +} + +func (i *importedString) Export() interface{} { + return i.s +} + +func (i *importedString) ExportType() reflect.Type { + return reflectTypeString +} + +func (i *importedString) baseObject(r *Runtime) *Object { + i.ensureScanned() + if i.u != nil { + return i.u.baseObject(r) + } + return asciiString(i.s).baseObject(r) +} + +func (i *importedString) hash(hasher *maphash.Hash) uint64 { + i.ensureScanned() + if i.u != nil { + return i.u.hash(hasher) + } + return asciiString(i.s).hash(hasher) +} + +func (i *importedString) CharAt(idx int) uint16 { + i.ensureScanned() + if i.u != nil { + return i.u.CharAt(idx) + } + return asciiString(i.s).CharAt(idx) +} + +func (i *importedString) Length() int { + i.ensureScanned() + if i.u != nil { + return i.u.Length() + } + return asciiString(i.s).Length() +} + +func (i *importedString) Concat(v String) String { + if !i.scanned { + if v, ok := v.(*importedString); ok { + if !v.scanned { + return &importedString{s: i.s + v.s} + } + } + i.ensureScanned() + } + if i.u != nil { + return i.u.Concat(v) + } + return asciiString(i.s).Concat(v) +} + +func (i *importedString) Substring(start, end int) String { + i.ensureScanned() + if i.u != nil { + return i.u.Substring(start, end) + } + return asciiString(i.s).Substring(start, end) +} + +func (i *importedString) CompareTo(v String) int { + return strings.Compare(i.s, v.String()) +} + +func (i *importedString) Reader() io.RuneReader { + if i.scanned { + if i.u != nil { + return i.u.Reader() + } + return asciiString(i.s).Reader() + } + return strings.NewReader(i.s) +} + +type stringUtf16Reader struct { + s string + pos int + second uint16 +} + +func (s *stringUtf16Reader) readChar() (c uint16, err error) { + if s.second != 0 { + c, s.second = s.second, 0 + return + } + if s.pos < len(s.s) { + r1, size1 := utf8.DecodeRuneInString(s.s[s.pos:]) + s.pos += size1 + if r1 <= 0xFFFF { + c = uint16(r1) + } else { + first, second := utf16.EncodeRune(r1) + c, s.second = uint16(first), uint16(second) + } + } else { + err = io.EOF + } + return +} + +func (s *stringUtf16Reader) ReadRune() (r rune, size int, err error) { + c, err := s.readChar() + if err != nil { + return + } + r = rune(c) + size = 1 + return +} + +func (i *importedString) utf16Reader() utf16Reader { + if i.scanned { + if i.u != nil { + return i.u.utf16Reader() + } + return asciiString(i.s).utf16Reader() + } + return &stringUtf16Reader{ + s: i.s, + } +} + +func (i *importedString) utf16RuneReader() io.RuneReader { + if i.scanned { + if i.u != nil { + return i.u.utf16RuneReader() + } + return asciiString(i.s).utf16RuneReader() + } + return &stringUtf16Reader{ + s: i.s, + } +} + +func (i *importedString) utf16Runes() []rune { + i.ensureScanned() + if i.u != nil { + return i.u.utf16Runes() + } + return asciiString(i.s).utf16Runes() +} + +func (i *importedString) index(v String, start int) int { + i.ensureScanned() + if i.u != nil { + return i.u.index(v, start) + } + return asciiString(i.s).index(v, start) +} + +func (i *importedString) lastIndex(v String, pos int) int { + i.ensureScanned() + if i.u != nil { + return i.u.lastIndex(v, pos) + } + return asciiString(i.s).lastIndex(v, pos) +} + +func (i *importedString) toLower() String { + i.ensureScanned() + if i.u != nil { + return toLower(i.s) + } + return asciiString(i.s).toLower() +} + +func (i *importedString) toUpper() String { + i.ensureScanned() + if i.u != nil { + caser := cases.Upper(language.Und) + return newStringValue(caser.String(i.s)) + } + return asciiString(i.s).toUpper() +} + +func (i *importedString) toTrimmedUTF8() string { + return strings.Trim(i.s, parser.WhitespaceChars) +} diff --git a/goja/string_test.go b/goja/string_test.go new file mode 100644 index 0000000..df242fc --- /dev/null +++ b/goja/string_test.go @@ -0,0 +1,194 @@ +package goja + +import ( + "strings" + "testing" + "unicode/utf16" +) + +func TestStringOOBProperties(t *testing.T) { + const SCRIPT = ` + var string = new String("str"); + + string[4] = 1; + string[4]; + ` + + testScript(SCRIPT, valueInt(1), t) +} + +func TestImportedString(t *testing.T) { + vm := New() + + testUnaryOp := func(a, expr string, result interface{}, t *testing.T) { + v, err := vm.RunString("a => " + expr) + if err != nil { + t.Fatal(err) + } + var fn func(a Value) (Value, error) + err = vm.ExportTo(v, &fn) + if err != nil { + t.Fatal(err) + } + for _, aa := range []Value{newStringValue(a), vm.ToValue(a)} { + res, err := fn(aa) + if err != nil { + t.Fatal(err) + } + if res.Export() != result { + t.Fatalf("%s, a:%v(%T). expected: %v, actual: %v", expr, aa, aa, result, res) + } + } + } + + testBinaryOp := func(a, b, expr string, result interface{}, t *testing.T) { + v, err := vm.RunString("(a, b) => " + expr) + if err != nil { + t.Fatal(err) + } + var fn func(a, b Value) (Value, error) + err = vm.ExportTo(v, &fn) + if err != nil { + t.Fatal(err) + } + for _, aa := range []Value{newStringValue(a), vm.ToValue(a)} { + for _, bb := range []Value{newStringValue(b), vm.ToValue(b)} { + res, err := fn(aa, bb) + if err != nil { + t.Fatal(err) + } + if res.Export() != result { + t.Fatalf("%s, a:%v(%T), b:%v(%T). expected: %v, actual: %v", expr, aa, aa, bb, bb, result, res) + } + } + } + } + + strs := []string{"shortAscii", "longlongAscii1234567890123456789", "short юникод", "long юникод 1234567890 юникод \U0001F600", "юникод", "Ascii", "long", "код"} + indexOfResults := [][]int{ + /* + const strs = ["shortAscii", "longlongAscii1234567890123456789", "short юникод", "long юникод 1234567890 юникод \u{1F600}", "юникод", "Ascii", "long", "код"]; + + strs.forEach(a => { + console.log("{", strs.map(b => a.indexOf(b)).join(", "), "},"); + }); + */ + {0, -1, -1, -1, -1, 5, -1, -1}, + {-1, 0, -1, -1, -1, 8, 0, -1}, + {-1, -1, 0, -1, 6, -1, -1, 9}, + {-1, -1, -1, 0, 5, -1, 0, 8}, + {-1, -1, -1, -1, 0, -1, -1, 3}, + {-1, -1, -1, -1, -1, 0, -1, -1}, + {-1, -1, -1, -1, -1, -1, 0, -1}, + {-1, -1, -1, -1, -1, -1, -1, 0}, + } + + lastIndexOfResults := [][]int{ + /* + strs.forEach(a => { + console.log("{", strs.map(b => a.lastIndexOf(b)).join(", "), "},"); + }); + */ + {0, -1, -1, -1, -1, 5, -1, -1}, + {-1, 0, -1, -1, -1, 8, 4, -1}, + {-1, -1, 0, -1, 6, -1, -1, 9}, + {-1, -1, -1, 0, 23, -1, 0, 26}, + {-1, -1, -1, -1, 0, -1, -1, 3}, + {-1, -1, -1, -1, -1, 0, -1, -1}, + {-1, -1, -1, -1, -1, -1, 0, -1}, + {-1, -1, -1, -1, -1, -1, -1, 0}, + } + + pad := func(s, p string, n int, start bool) string { + if n == 0 { + return s + } + if p == "" { + p = " " + } + var b strings.Builder + ss := utf16.Encode([]rune(s)) + b.Grow(n) + n -= len(ss) + if !start { + b.WriteString(s) + } + if n > 0 { + pp := utf16.Encode([]rune(p)) + for n > 0 { + if n > len(pp) { + b.WriteString(p) + n -= len(pp) + } else { + b.WriteString(string(utf16.Decode(pp[:n]))) + n = 0 + } + } + } + if start { + b.WriteString(s) + } + return b.String() + } + + for i, a := range strs { + testUnaryOp(a, "JSON.parse(JSON.stringify(a))", a, t) + testUnaryOp(a, "a.length", int64(len(utf16.Encode([]rune(a)))), t) + for j, b := range strs { + testBinaryOp(a, b, "a === b", a == b, t) + testBinaryOp(a, b, "a == b", a == b, t) + testBinaryOp(a, b, "a + b", a+b, t) + testBinaryOp(a, b, "a > b", strings.Compare(a, b) > 0, t) + testBinaryOp(a, b, "`A${a}B${b}C`", "A"+a+"B"+b+"C", t) + testBinaryOp(a, b, "a.indexOf(b)", int64(indexOfResults[i][j]), t) + testBinaryOp(a, b, "a.lastIndexOf(b)", int64(lastIndexOfResults[i][j]), t) + testBinaryOp(a, b, "a.padStart(32, b)", pad(a, b, 32, true), t) + testBinaryOp(a, b, "a.padEnd(32, b)", pad(a, b, 32, false), t) + testBinaryOp(a, b, "a.replace(b, '')", strings.Replace(a, b, "", 1), t) + } + } +} + +func TestStringFromUTF16(t *testing.T) { + s := StringFromUTF16([]uint16{}) + if s.Length() != 0 || !s.SameAs(asciiString("")) { + t.Fatal(s) + } + + s = StringFromUTF16([]uint16{0xD800}) + if s.Length() != 1 || s.CharAt(0) != 0xD800 { + t.Fatal(s) + } + + s = StringFromUTF16([]uint16{'A', 'B'}) + if !s.SameAs(asciiString("AB")) { + t.Fatal(s) + } +} + +func TestStringBuilder(t *testing.T) { + t.Run("writeUTF8String-switch", func(t *testing.T) { + var sb StringBuilder + sb.WriteUTF8String("Head") + sb.WriteUTF8String("1ábc") + if res := sb.String().String(); res != "Head1ábc" { + t.Fatal(res) + } + }) +} + +func BenchmarkASCIIConcat(b *testing.B) { + vm := New() + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _, err := vm.RunString(`{let result = "ab"; + for (let i = 0 ; i < 10;i++) { + result += result; + }}`) + if err != nil { + b.Fatalf("Unexpected errors %s", err) + } + } +} diff --git a/goja/string_unicode.go b/goja/string_unicode.go new file mode 100644 index 0000000..49e363f --- /dev/null +++ b/goja/string_unicode.go @@ -0,0 +1,619 @@ +package goja + +import ( + "errors" + "hash/maphash" + "io" + "math" + "reflect" + "strings" + "unicode/utf16" + "unicode/utf8" + + "github.com/dop251/goja/parser" + "github.com/dop251/goja/unistring" + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +type unicodeString []uint16 + +type unicodeRuneReader struct { + s unicodeString + pos int +} + +type utf16RuneReader struct { + s unicodeString + pos int +} + +// passes through invalid surrogate pairs +type lenientUtf16Decoder struct { + utf16Reader utf16Reader + prev uint16 + prevSet bool +} + +// StringBuilder serves similar purpose to strings.Builder, except it works with ECMAScript String. +// Use it to efficiently build 'native' ECMAScript values that either contain invalid UTF-16 surrogate pairs +// (and therefore cannot be represented as UTF-8) or never expected to be exported to Go. See also +// StringFromUTF16. +type StringBuilder struct { + asciiBuilder strings.Builder + unicodeBuilder unicodeStringBuilder +} + +type unicodeStringBuilder struct { + buf []uint16 + unicode bool +} + +var ( + InvalidRuneError = errors.New("invalid rune") +) + +func (rr *utf16RuneReader) readChar() (c uint16, err error) { + if rr.pos < len(rr.s) { + c = rr.s[rr.pos] + rr.pos++ + return + } + err = io.EOF + return +} + +func (rr *utf16RuneReader) ReadRune() (r rune, size int, err error) { + if rr.pos < len(rr.s) { + r = rune(rr.s[rr.pos]) + rr.pos++ + size = 1 + return + } + err = io.EOF + return +} + +func (rr *lenientUtf16Decoder) ReadRune() (r rune, size int, err error) { + var c uint16 + if rr.prevSet { + c = rr.prev + rr.prevSet = false + } else { + c, err = rr.utf16Reader.readChar() + if err != nil { + return + } + } + size = 1 + if isUTF16FirstSurrogate(c) { + second, err1 := rr.utf16Reader.readChar() + if err1 != nil { + if err1 != io.EOF { + err = err1 + } else { + r = rune(c) + } + return + } + if isUTF16SecondSurrogate(second) { + r = utf16.DecodeRune(rune(c), rune(second)) + size++ + return + } else { + rr.prev = second + rr.prevSet = true + } + } + r = rune(c) + return +} + +func (rr *unicodeRuneReader) ReadRune() (r rune, size int, err error) { + if rr.pos < len(rr.s) { + c := rr.s[rr.pos] + size++ + rr.pos++ + if isUTF16FirstSurrogate(c) { + if rr.pos < len(rr.s) { + second := rr.s[rr.pos] + if isUTF16SecondSurrogate(second) { + r = utf16.DecodeRune(rune(c), rune(second)) + size++ + rr.pos++ + return + } + } + err = InvalidRuneError + } else if isUTF16SecondSurrogate(c) { + err = InvalidRuneError + } + r = rune(c) + } else { + err = io.EOF + } + return +} + +func (b *unicodeStringBuilder) Grow(n int) { + if len(b.buf) == 0 { + n++ + } + if cap(b.buf)-len(b.buf) < n { + buf := make([]uint16, len(b.buf), 2*cap(b.buf)+n) + copy(buf, b.buf) + b.buf = buf + } +} + +func (b *unicodeStringBuilder) ensureStarted(initialSize int) { + b.Grow(initialSize) + if len(b.buf) == 0 { + b.buf = append(b.buf, unistring.BOM) + } +} + +// assumes already started +func (b *unicodeStringBuilder) writeString(s String) { + a, u := devirtualizeString(s) + if u != nil { + b.buf = append(b.buf, u[1:]...) + b.unicode = true + } else { + for i := 0; i < len(a); i++ { + b.buf = append(b.buf, uint16(a[i])) + } + } +} + +func (b *unicodeStringBuilder) String() String { + if b.unicode { + return unicodeString(b.buf) + } + if len(b.buf) < 2 { + return stringEmpty + } + buf := make([]byte, 0, len(b.buf)-1) + for _, c := range b.buf[1:] { + buf = append(buf, byte(c)) + } + return asciiString(buf) +} + +func (b *unicodeStringBuilder) WriteRune(r rune) { + b.ensureStarted(2) + b.writeRuneFast(r) +} + +// assumes already started +func (b *unicodeStringBuilder) writeRuneFast(r rune) { + if r <= 0xFFFF { + b.buf = append(b.buf, uint16(r)) + if !b.unicode && r >= utf8.RuneSelf { + b.unicode = true + } + } else { + first, second := utf16.EncodeRune(r) + b.buf = append(b.buf, uint16(first), uint16(second)) + b.unicode = true + } +} + +func (b *unicodeStringBuilder) writeASCIIString(bytes string) { + for _, c := range bytes { + b.buf = append(b.buf, uint16(c)) + } +} + +func (b *unicodeStringBuilder) writeUnicodeString(str unicodeString) { + b.buf = append(b.buf, str[1:]...) + b.unicode = true +} + +func (b *StringBuilder) ascii() bool { + return len(b.unicodeBuilder.buf) == 0 +} + +func (b *StringBuilder) WriteString(s String) { + a, u := devirtualizeString(s) + if u != nil { + b.switchToUnicode(u.Length()) + b.unicodeBuilder.writeUnicodeString(u) + } else { + if b.ascii() { + b.asciiBuilder.WriteString(string(a)) + } else { + b.unicodeBuilder.writeASCIIString(string(a)) + } + } +} + +func (b *StringBuilder) WriteUTF8String(s string) { + firstUnicodeIdx := 0 + if b.ascii() { + for i := 0; i < len(s); i++ { + if s[i] >= utf8.RuneSelf { + b.switchToUnicode(len(s)) + b.unicodeBuilder.writeASCIIString(s[:i]) + firstUnicodeIdx = i + goto unicode + } + } + b.asciiBuilder.WriteString(s) + return + } +unicode: + for _, r := range s[firstUnicodeIdx:] { + b.unicodeBuilder.writeRuneFast(r) + } +} + +func (b *StringBuilder) writeASCII(s string) { + if b.ascii() { + b.asciiBuilder.WriteString(s) + } else { + b.unicodeBuilder.writeASCIIString(s) + } +} + +func (b *StringBuilder) WriteRune(r rune) { + if r < utf8.RuneSelf { + if b.ascii() { + b.asciiBuilder.WriteByte(byte(r)) + } else { + b.unicodeBuilder.writeRuneFast(r) + } + } else { + var extraLen int + if r <= 0xFFFF { + extraLen = 1 + } else { + extraLen = 2 + } + b.switchToUnicode(extraLen) + b.unicodeBuilder.writeRuneFast(r) + } +} + +func (b *StringBuilder) String() String { + if b.ascii() { + return asciiString(b.asciiBuilder.String()) + } + return b.unicodeBuilder.String() +} + +func (b *StringBuilder) Grow(n int) { + if b.ascii() { + b.asciiBuilder.Grow(n) + } else { + b.unicodeBuilder.Grow(n) + } +} + +// LikelyUnicode hints to the builder that the resulting string is likely to contain Unicode (non-ASCII) characters. +// The argument is an extra capacity (in characters) to reserve on top of the current length (it's like calling +// Grow() afterwards). +// This method may be called at any point (not just when the buffer is empty), although for efficiency it should +// be called as early as possible. +func (b *StringBuilder) LikelyUnicode(extraLen int) { + b.switchToUnicode(extraLen) +} + +func (b *StringBuilder) switchToUnicode(extraLen int) { + if b.ascii() { + c := b.asciiBuilder.Cap() + newCap := b.asciiBuilder.Len() + extraLen + if newCap < c { + newCap = c + } + b.unicodeBuilder.ensureStarted(newCap) + b.unicodeBuilder.writeASCIIString(b.asciiBuilder.String()) + b.asciiBuilder.Reset() + } +} + +func (b *StringBuilder) WriteSubstring(source String, start int, end int) { + a, us := devirtualizeString(source) + if us == nil { + if b.ascii() { + b.asciiBuilder.WriteString(string(a[start:end])) + } else { + b.unicodeBuilder.writeASCIIString(string(a[start:end])) + } + return + } + if b.ascii() { + uc := false + for i := start; i < end; i++ { + if us.CharAt(i) >= utf8.RuneSelf { + uc = true + break + } + } + if uc { + b.switchToUnicode(end - start + 1) + } else { + b.asciiBuilder.Grow(end - start + 1) + for i := start; i < end; i++ { + b.asciiBuilder.WriteByte(byte(us.CharAt(i))) + } + return + } + } + b.unicodeBuilder.buf = append(b.unicodeBuilder.buf, us[start+1:end+1]...) + b.unicodeBuilder.unicode = true +} + +func (s unicodeString) Reader() io.RuneReader { + return &unicodeRuneReader{ + s: s[1:], + } +} + +func (s unicodeString) utf16Reader() utf16Reader { + return &utf16RuneReader{ + s: s[1:], + } +} + +func (s unicodeString) utf16RuneReader() io.RuneReader { + return &utf16RuneReader{ + s: s[1:], + } +} + +func (s unicodeString) utf16Runes() []rune { + runes := make([]rune, len(s)-1) + for i, ch := range s[1:] { + runes[i] = rune(ch) + } + return runes +} + +func (s unicodeString) ToInteger() int64 { + return 0 +} + +func (s unicodeString) toString() String { + return s +} + +func (s unicodeString) ToString() Value { + return s +} + +func (s unicodeString) ToFloat() float64 { + return math.NaN() +} + +func (s unicodeString) ToBoolean() bool { + return len(s) > 0 +} + +func (s unicodeString) toTrimmedUTF8() string { + if len(s) == 0 { + return "" + } + return strings.Trim(s.String(), parser.WhitespaceChars) +} + +func (s unicodeString) ToNumber() Value { + return asciiString(s.toTrimmedUTF8()).ToNumber() +} + +func (s unicodeString) ToObject(r *Runtime) *Object { + return r._newString(s, r.getStringPrototype()) +} + +func (s unicodeString) equals(other unicodeString) bool { + if len(s) != len(other) { + return false + } + for i, r := range s { + if r != other[i] { + return false + } + } + return true +} + +func (s unicodeString) SameAs(other Value) bool { + return s.StrictEquals(other) +} + +func (s unicodeString) Equals(other Value) bool { + if s.StrictEquals(other) { + return true + } + + if o, ok := other.(*Object); ok { + return s.Equals(o.toPrimitive()) + } + return false +} + +func (s unicodeString) StrictEquals(other Value) bool { + if otherStr, ok := other.(unicodeString); ok { + return s.equals(otherStr) + } + if otherStr, ok := other.(*importedString); ok { + otherStr.ensureScanned() + if otherStr.u != nil { + return s.equals(otherStr.u) + } + } + + return false +} + +func (s unicodeString) baseObject(r *Runtime) *Object { + ss := r.getStringSingleton() + ss.value = s + ss.setLength() + return ss.val +} + +func (s unicodeString) CharAt(idx int) uint16 { + return s[idx+1] +} + +func (s unicodeString) Length() int { + return len(s) - 1 +} + +func (s unicodeString) Concat(other String) String { + a, u := devirtualizeString(other) + if u != nil { + b := make(unicodeString, len(s)+len(u)-1) + copy(b, s) + copy(b[len(s):], u[1:]) + return b + } + b := make([]uint16, len(s)+len(a)) + copy(b, s) + b1 := b[len(s):] + for i := 0; i < len(a); i++ { + b1[i] = uint16(a[i]) + } + return unicodeString(b) +} + +func (s unicodeString) Substring(start, end int) String { + ss := s[start+1 : end+1] + for _, c := range ss { + if c >= utf8.RuneSelf { + b := make(unicodeString, end-start+1) + b[0] = unistring.BOM + copy(b[1:], ss) + return b + } + } + as := make([]byte, end-start) + for i, c := range ss { + as[i] = byte(c) + } + return asciiString(as) +} + +func (s unicodeString) String() string { + return string(utf16.Decode(s[1:])) +} + +func (s unicodeString) CompareTo(other String) int { + // TODO handle invalid UTF-16 + return strings.Compare(s.String(), other.String()) +} + +func (s unicodeString) index(substr String, start int) int { + var ss []uint16 + a, u := devirtualizeString(substr) + if u != nil { + ss = u[1:] + } else { + ss = make([]uint16, len(a)) + for i := 0; i < len(a); i++ { + ss[i] = uint16(a[i]) + } + } + s1 := s[1:] + // TODO: optimise + end := len(s1) - len(ss) + for start <= end { + for i := 0; i < len(ss); i++ { + if s1[start+i] != ss[i] { + goto nomatch + } + } + + return start + nomatch: + start++ + } + return -1 +} + +func (s unicodeString) lastIndex(substr String, start int) int { + var ss []uint16 + a, u := devirtualizeString(substr) + if u != nil { + ss = u[1:] + } else { + ss = make([]uint16, len(a)) + for i := 0; i < len(a); i++ { + ss[i] = uint16(a[i]) + } + } + + s1 := s[1:] + if maxStart := len(s1) - len(ss); start > maxStart { + start = maxStart + } + // TODO: optimise + for start >= 0 { + for i := 0; i < len(ss); i++ { + if s1[start+i] != ss[i] { + goto nomatch + } + } + + return start + nomatch: + start-- + } + return -1 +} + +func unicodeStringFromRunes(r []rune) unicodeString { + return unistring.NewFromRunes(r).AsUtf16() +} + +func toLower(s string) String { + caser := cases.Lower(language.Und) + r := []rune(caser.String(s)) + // Workaround + ascii := true + for i := 0; i < len(r)-1; i++ { + if (i == 0 || r[i-1] != 0x3b1) && r[i] == 0x345 && r[i+1] == 0x3c2 { + i++ + r[i] = 0x3c3 + } + if r[i] >= utf8.RuneSelf { + ascii = false + } + } + if ascii { + ascii = r[len(r)-1] < utf8.RuneSelf + } + if ascii { + return asciiString(r) + } + return unicodeStringFromRunes(r) +} + +func (s unicodeString) toLower() String { + return toLower(s.String()) +} + +func (s unicodeString) toUpper() String { + caser := cases.Upper(language.Und) + return newStringValue(caser.String(s.String())) +} + +func (s unicodeString) Export() interface{} { + return s.String() +} + +func (s unicodeString) ExportType() reflect.Type { + return reflectTypeString +} + +func (s unicodeString) hash(hash *maphash.Hash) uint64 { + _, _ = hash.WriteString(string(unistring.FromUtf16(s))) + h := hash.Sum64() + hash.Reset() + return h +} + +func (s unicodeString) string() unistring.String { + return unistring.FromUtf16(s) +} diff --git a/goja/tc39_norace_test.go b/goja/tc39_norace_test.go new file mode 100644 index 0000000..9a1a18c --- /dev/null +++ b/goja/tc39_norace_test.go @@ -0,0 +1,19 @@ +//go:build !race +// +build !race + +package goja + +import "testing" + +// Prevent linter warnings about unused type +var _ = tc39Test{name: "", f: nil} + +func (ctx *tc39TestCtx) runTest(name string, f func(t *testing.T)) { + ctx.t.Run(name, func(t *testing.T) { + t.Parallel() + f(t) + }) +} + +func (ctx *tc39TestCtx) flush() { +} diff --git a/goja/tc39_race_test.go b/goja/tc39_race_test.go new file mode 100644 index 0000000..e17bd12 --- /dev/null +++ b/goja/tc39_race_test.go @@ -0,0 +1,32 @@ +//go:build race +// +build race + +package goja + +import ( + "testing" +) + +const ( + tc39MaxTestGroupSize = 8000 // to prevent race detector complaining about too many goroutines +) + +func (ctx *tc39TestCtx) runTest(name string, f func(t *testing.T)) { + ctx.testQueue = append(ctx.testQueue, tc39Test{name: name, f: f}) + if len(ctx.testQueue) >= tc39MaxTestGroupSize { + ctx.flush() + } +} + +func (ctx *tc39TestCtx) flush() { + ctx.t.Run("tc39", func(t *testing.T) { + for _, tc := range ctx.testQueue { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + tc.f(t) + }) + } + }) + ctx.testQueue = ctx.testQueue[:0] +} diff --git a/goja/tc39_test.go b/goja/tc39_test.go new file mode 100644 index 0000000..ac60375 --- /dev/null +++ b/goja/tc39_test.go @@ -0,0 +1,755 @@ +package goja + +import ( + "errors" + "fmt" + "io" + "os" + "path" + "runtime/debug" + "sort" + "strings" + "sync" + "testing" + "time" + + "github.com/Masterminds/semver/v3" + "gopkg.in/yaml.v2" +) + +const ( + tc39BASE = "testdata/test262" +) + +var ( + invalidFormatError = errors.New("Invalid file format") + + ignorableTestError = newSymbol(stringEmpty) +) + +var ( + skipPrefixes prefixList + + skipList = map[string]bool{ + + // out-of-date (https://github.com/tc39/test262/issues/3407) + "test/language/expressions/prefix-increment/S11.4.4_A6_T3.js": true, + "test/language/expressions/prefix-increment/S11.4.4_A6_T2.js": true, + "test/language/expressions/prefix-increment/S11.4.4_A6_T1.js": true, + "test/language/expressions/prefix-decrement/S11.4.5_A6_T3.js": true, + "test/language/expressions/prefix-decrement/S11.4.5_A6_T2.js": true, + "test/language/expressions/prefix-decrement/S11.4.5_A6_T1.js": true, + "test/language/expressions/postfix-increment/S11.3.1_A6_T3.js": true, + "test/language/expressions/postfix-increment/S11.3.1_A6_T2.js": true, + "test/language/expressions/postfix-increment/S11.3.1_A6_T1.js": true, + "test/language/expressions/postfix-decrement/S11.3.2_A6_T3.js": true, + "test/language/expressions/postfix-decrement/S11.3.2_A6_T2.js": true, + "test/language/expressions/postfix-decrement/S11.3.2_A6_T1.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.1_T4.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.1_T2.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.1_T1.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.11_T4.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.11_T2.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.11_T1.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.10_T4.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.10_T2.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.10_T1.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.9_T4.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.9_T2.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.9_T1.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.8_T4.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.8_T2.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.8_T1.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.7_T4.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.7_T2.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.7_T1.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.6_T4.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.6_T2.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.6_T1.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.5_T4.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.5_T2.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.5_T1.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.4_T4.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.4_T2.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.4_T1.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.3_T4.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.3_T2.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.3_T1.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.2_T4.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.2_T2.js": true, + "test/language/expressions/compound-assignment/S11.13.2_A7.2_T1.js": true, + "test/language/expressions/assignment/S11.13.1_A7_T3.js": true, + + // timezone + "test/built-ins/Date/prototype/toISOString/15.9.5.43-0-8.js": true, + "test/built-ins/Date/prototype/toISOString/15.9.5.43-0-9.js": true, + "test/built-ins/Date/prototype/toISOString/15.9.5.43-0-10.js": true, + + // floating point date calculations + "test/built-ins/Date/UTC/fp-evaluation-order.js": true, + + // quantifier integer limit in regexp + "test/built-ins/RegExp/quantifier-integer-limit.js": true, + + // GetFunctionRealm + "test/built-ins/Function/internals/Construct/base-ctor-revoked-proxy.js": true, + + // Uses deprecated __lookupGetter__/__lookupSetter__ + "test/language/expressions/class/elements/private-getter-is-not-a-own-property.js": true, + "test/language/expressions/class/elements/private-setter-is-not-a-own-property.js": true, + "test/language/statements/class/elements/private-setter-is-not-a-own-property.js": true, + "test/language/statements/class/elements/private-getter-is-not-a-own-property.js": true, + + // restricted unicode regexp syntax + "test/built-ins/RegExp/unicode_restricted_quantifiable_assertion.js": true, + "test/built-ins/RegExp/unicode_restricted_octal_escape.js": true, + "test/built-ins/RegExp/unicode_restricted_incomple_quantifier.js": true, + "test/built-ins/RegExp/unicode_restricted_incomplete_quantifier.js": true, + "test/built-ins/RegExp/unicode_restricted_identity_escape_x.js": true, + "test/built-ins/RegExp/unicode_restricted_identity_escape_u.js": true, + "test/built-ins/RegExp/unicode_restricted_identity_escape_c.js": true, + "test/built-ins/RegExp/unicode_restricted_identity_escape_alpha.js": true, + "test/built-ins/RegExp/unicode_restricted_identity_escape.js": true, + "test/built-ins/RegExp/unicode_restricted_brackets.js": true, + "test/built-ins/RegExp/unicode_restricted_character_class_escape.js": true, + "test/annexB/built-ins/RegExp/prototype/compile/pattern-string-invalid-u.js": true, + + // Because goja parser works in UTF-8 it is not possible to pass strings containing invalid UTF-16 code points. + // This is mitigated by escaping them as \uXXXX, however because of this the RegExp source becomes + // `\uXXXX` instead of ``. + // The resulting RegExp will work exactly the same, but it causes these two tests to fail. + "test/annexB/built-ins/RegExp/RegExp-leading-escape-BMP.js": true, + "test/annexB/built-ins/RegExp/RegExp-trailing-escape-BMP.js": true, + "test/language/literals/regexp/S7.8.5_A1.4_T2.js": true, + "test/language/literals/regexp/S7.8.5_A1.1_T2.js": true, + "test/language/literals/regexp/S7.8.5_A2.1_T2.js": true, + "test/language/literals/regexp/S7.8.5_A2.4_T2.js": true, + + // async generator + "test/language/expressions/optional-chaining/member-expression.js": true, + "test/language/expressions/class/elements/same-line-async-method-rs-static-async-generator-method-privatename-identifier-alt.js": true, + "test/language/expressions/class/elements/same-line-async-method-rs-static-async-generator-method-privatename-identifier.js": true, + "test/language/destructuring/binding/syntax/destructuring-object-parameters-function-arguments-length.js": true, + "test/language/destructuring/binding/syntax/destructuring-array-parameters-function-arguments-length.js": true, + "test/language/comments/hashbang/function-constructor.js": true, + "test/language/statements/class/elements/after-same-line-static-async-method-rs-static-async-generator-method-privatename-identifier.js": true, + "test/language/statements/class/elements/after-same-line-static-async-method-rs-static-async-generator-method-privatename-identifier-alt.js": true, + "test/language/statements/class/elements/same-line-async-method-rs-static-async-generator-method-privatename-identifier.js": true, + "test/language/statements/class/elements/same-line-async-method-rs-static-async-generator-method-privatename-identifier-alt.js": true, + "test/language/expressions/class/elements/after-same-line-static-async-method-rs-static-async-generator-method-privatename-identifier-alt.js": true, + "test/language/expressions/class/elements/after-same-line-static-async-method-rs-static-async-generator-method-privatename-identifier.js": true, + "test/built-ins/Object/seal/seal-asyncgeneratorfunction.js": true, + "test/language/statements/switch/scope-lex-async-generator.js": true, + "test/language/statements/class/elements/private-async-generator-method-name.js": true, + "test/language/expressions/class/elements/private-async-generator-method-name.js": true, + "test/language/expressions/async-generator/name.js": true, + "test/language/statements/class/elements/same-line-gen-rs-static-async-generator-method-privatename-identifier.js": true, + "test/language/statements/class/elements/same-line-gen-rs-static-async-generator-method-privatename-identifier-alt.js": true, + "test/language/statements/class/elements/new-sc-line-gen-rs-static-async-generator-method-privatename-identifier.js": true, + "test/language/statements/class/elements/new-sc-line-gen-rs-static-async-generator-method-privatename-identifier-alt.js": true, + "test/language/statements/class/elements/after-same-line-static-gen-rs-static-async-generator-method-privatename-identifier.js": true, + "test/language/statements/class/elements/after-same-line-static-gen-rs-static-async-generator-method-privatename-identifier-alt.js": true, + "test/language/statements/class/elements/after-same-line-gen-rs-static-async-generator-method-privatename-identifier.js": true, + "test/language/statements/class/elements/after-same-line-gen-rs-static-async-generator-method-privatename-identifier-alt.js": true, + "test/language/expressions/class/elements/same-line-gen-rs-static-async-generator-method-privatename-identifier.js": true, + "test/language/expressions/class/elements/same-line-gen-rs-static-async-generator-method-privatename-identifier-alt.js": true, + "test/language/expressions/class/elements/new-sc-line-gen-rs-static-async-generator-method-privatename-identifier.js": true, + "test/language/expressions/class/elements/new-sc-line-gen-rs-static-async-generator-method-privatename-identifier-alt.js": true, + "test/language/expressions/class/elements/after-same-line-static-gen-rs-static-async-generator-method-privatename-identifier.js": true, + "test/language/expressions/class/elements/after-same-line-static-gen-rs-static-async-generator-method-privatename-identifier-alt.js": true, + "test/language/expressions/class/elements/after-same-line-gen-rs-static-async-generator-method-privatename-identifier.js": true, + "test/language/expressions/class/elements/after-same-line-gen-rs-static-async-generator-method-privatename-identifier-alt.js": true, + "test/built-ins/GeneratorFunction/is-a-constructor.js": true, + + // async iterator + "test/language/expressions/optional-chaining/iteration-statement-for-await-of.js": true, + + // legacy number literals + "test/language/literals/numeric/non-octal-decimal-integer.js": true, + "test/language/literals/string/S7.8.4_A4.3_T2.js": true, + "test/language/literals/string/S7.8.4_A4.3_T1.js": true, + + // Regexp + "test/language/literals/regexp/invalid-range-negative-lookbehind.js": true, + "test/language/literals/regexp/invalid-range-lookbehind.js": true, + "test/language/literals/regexp/invalid-optional-negative-lookbehind.js": true, + "test/language/literals/regexp/invalid-optional-lookbehind.js": true, + + // unicode full case folding + "test/built-ins/RegExp/unicode_full_case_folding.js": true, + + // FIXME bugs + + // Left-hand side as a CoverParenthesizedExpression + "test/language/expressions/assignment/fn-name-lhs-cover.js": true, + + // Character \ missing from character class [\c] + "test/annexB/built-ins/RegExp/RegExp-invalid-control-escape-character-class.js": true, + "test/annexB/built-ins/RegExp/RegExp-control-escape-russian-letter.js": true, + + // Skip due to regexp named groups + "test/built-ins/String/prototype/replaceAll/searchValue-replacer-RegExp-call.js": true, + + "test/built-ins/RegExp/nullable-quantifier.js": true, + "test/built-ins/RegExp/lookahead-quantifier-match-groups.js": true, + } + + featuresBlackList = []string{ + "async-iteration", + "Symbol.asyncIterator", + "resizable-arraybuffer", + "regexp-named-groups", + "regexp-duplicate-named-groups", + "regexp-unicode-property-escapes", + "regexp-match-indices", + "regexp-modifiers", + "RegExp.escape", + "legacy-regexp", + "tail-call-optimization", + "Temporal", + "import-assertions", + "dynamic-import", + "logical-assignment-operators", + "import.meta", + "Atomics", + "Atomics.waitAsync", + "Atomics.pause", + "FinalizationRegistry", + "WeakRef", + "__getter__", + "__setter__", + "ShadowRealm", + "SharedArrayBuffer", + "decorators", + "regexp-v-flag", + "iterator-helpers", + "symbols-as-weakmap-keys", + "uint8array-base64", + "String.prototype.toWellFormed", + "explicit-resource-management", + "set-methods", + "promise-try", + "promise-with-resolvers", + "array-grouping", + "Math.sumPrecise", + "Float16Array", + "arraybuffer-transfer", + "Array.fromAsync", + "String.prototype.isWellFormed", + } +) + +var goVersion *semver.Version + +func init() { + if info, ok := debug.ReadBuildInfo(); ok { + goVersion = semver.MustParse(strings.TrimPrefix(info.GoVersion, "go")) + } else { + panic("Could not read build info") + } + + skip := func(prefixes ...string) { + for _, prefix := range prefixes { + skipPrefixes.Add(prefix) + } + } + + if goVersion.LessThan(semver.MustParse("1.21")) { + skip( + // Go <1.21 only supports Unicode 13 + "test/language/identifiers/start-unicode-14.", + "test/language/identifiers/part-unicode-14.", + "test/language/identifiers/start-unicode-15.", + "test/language/identifiers/part-unicode-15.", + ) + } + + skip( + // generators and async generators (harness/hidden-constructors.js) + "test/built-ins/Async", + + // async generators + "test/language/statements/class/elements/wrapped-in-sc-rs-static-async-generator-", + "test/language/statements/class/elements/same-line-method-rs-static-async-generator-", + "test/language/statements/class/elements/regular-definitions-rs-static-async-generator-", + "test/language/statements/class/elements/private-static-async-generator-", + "test/language/statements/class/elements/new-sc-line-method-rs-static-async-generator-", + "test/language/statements/class/elements/multiple-stacked-definitions-rs-static-async-generator-", + "test/language/statements/class/elements/new-no-sc-line-method-rs-static-async-generator-", + "test/language/statements/class/elements/multiple-definitions-rs-static-async-generator-", + "test/language/statements/class/elements/after-same-line-static-method-rs-static-async-generator-", + "test/language/statements/class/elements/after-same-line-method-rs-static-async-generator-", + "test/language/statements/class/elements/after-same-line-static-method-rs-static-async-generator-", + + "test/language/expressions/class/elements/wrapped-in-sc-rs-static-async-generator-", + "test/language/expressions/class/elements/same-line-method-rs-static-async-generator-", + "test/language/expressions/class/elements/regular-definitions-rs-static-async-generator-", + "test/language/expressions/class/elements/private-static-async-generator-", + "test/language/expressions/class/elements/new-sc-line-method-rs-static-async-generator-", + "test/language/expressions/class/elements/multiple-stacked-definitions-rs-static-async-generator-", + "test/language/expressions/class/elements/new-no-sc-line-method-rs-static-async-generator-", + "test/language/expressions/class/elements/multiple-definitions-rs-static-async-generator-", + "test/language/expressions/class/elements/after-same-line-static-method-rs-static-async-generator-", + "test/language/expressions/class/elements/after-same-line-method-rs-static-async-generator-", + "test/language/expressions/class/elements/after-same-line-static-method-rs-static-async-generator-", + + "test/language/eval-code/direct/async-gen-", + + // restricted unicode regexp syntax + "test/language/literals/regexp/u-", + + // legacy octal escape in strings in strict mode + "test/language/literals/string/legacy-octal-", + "test/language/literals/string/legacy-non-octal-", + + // modules + "test/language/export/", + "test/language/import/", + "test/language/module-code/", + ) + +} + +type tc39Test struct { + name string + f func(t *testing.T) +} + +type tc39BenchmarkItem struct { + name string + duration time.Duration +} + +type tc39BenchmarkData []tc39BenchmarkItem + +type tc39TestCtx struct { + base string + t *testing.T + prgCache map[string]*Program + prgCacheLock sync.Mutex + enableBench bool + benchmark tc39BenchmarkData + benchLock sync.Mutex + sabStub *Program + //lint:ignore U1000 Only used with race + testQueue []tc39Test +} + +type TC39MetaNegative struct { + Phase, Type string +} + +type tc39Meta struct { + Negative TC39MetaNegative + Includes []string + Flags []string + Features []string + Es5id string + Es6id string + Esid string +} + +type prefixList struct { + prefixes map[int]map[string]struct{} +} + +func (pl *prefixList) Add(prefix string) { + l := pl.prefixes[len(prefix)] + if l == nil { + l = make(map[string]struct{}) + if pl.prefixes == nil { + pl.prefixes = make(map[int]map[string]struct{}) + } + pl.prefixes[len(prefix)] = l + } + l[prefix] = struct{}{} +} + +func (pl *prefixList) Match(s string) bool { + for l, prefixes := range pl.prefixes { + if len(s) >= l { + if _, exists := prefixes[s[:l]]; exists { + return true + } + } + } + return false +} + +func (m *tc39Meta) hasFlag(flag string) bool { + for _, f := range m.Flags { + if f == flag { + return true + } + } + return false +} + +func parseTC39File(name string) (*tc39Meta, string, error) { + f, err := os.Open(name) + if err != nil { + return nil, "", err + } + defer f.Close() + + b, err := io.ReadAll(f) + if err != nil { + return nil, "", err + } + + str := string(b) + metaStart := strings.Index(str, "/*---") + if metaStart == -1 { + return nil, "", invalidFormatError + } else { + metaStart += 5 + } + metaEnd := strings.Index(str, "---*/") + if metaEnd == -1 || metaEnd <= metaStart { + return nil, "", invalidFormatError + } + + var meta tc39Meta + err = yaml.Unmarshal([]byte(str[metaStart:metaEnd]), &meta) + if err != nil { + return nil, "", err + } + + if meta.Negative.Type != "" && meta.Negative.Phase == "" { + return nil, "", errors.New("negative type is set, but phase isn't") + } + + return &meta, str, nil +} + +func (*tc39TestCtx) detachArrayBuffer(call FunctionCall) Value { + if obj, ok := call.Argument(0).(*Object); ok { + if buf, ok := obj.self.(*arrayBufferObject); ok { + buf.detach() + return _undefined + } + } + panic(typeError("detachArrayBuffer() is called with incompatible argument")) +} + +func (*tc39TestCtx) throwIgnorableTestError(FunctionCall) Value { + panic(ignorableTestError) +} + +func (ctx *tc39TestCtx) runTC39Test(name, src string, meta *tc39Meta, t testing.TB) { + defer func() { + if x := recover(); x != nil { + panic(fmt.Sprintf("panic while running %s: %v", name, x)) + } + }() + vm := New() + _262 := vm.NewObject() + _262.Set("detachArrayBuffer", ctx.detachArrayBuffer) + _262.Set("createRealm", ctx.throwIgnorableTestError) + _262.Set("evalScript", func(call FunctionCall) Value { + script := call.Argument(0).String() + result, err := vm.RunString(script) + if err != nil { + panic(err) + } + return result + }) + vm.Set("$262", _262) + vm.Set("IgnorableTestError", ignorableTestError) + vm.RunProgram(ctx.sabStub) + var out []string + async := meta.hasFlag("async") + if async { + err := ctx.runFile(ctx.base, path.Join("harness", "doneprintHandle.js"), vm) + if err != nil { + t.Fatal(err) + } + vm.Set("print", func(msg string) { + out = append(out, msg) + }) + } else { + vm.Set("print", t.Log) + } + + err, early := ctx.runTC39Script(name, src, meta.Includes, vm) + + if err != nil { + if meta.Negative.Type == "" { + if err, ok := err.(*Exception); ok { + if err.Value() == ignorableTestError { + t.Skip("Test threw IgnorableTestError") + } + } + t.Fatalf("%s: %v", name, err) + } else { + if (meta.Negative.Phase == "early" || meta.Negative.Phase == "parse") && !early || meta.Negative.Phase == "runtime" && early { + t.Fatalf("%s: error %v happened at the wrong phase (expected %s)", name, err, meta.Negative.Phase) + } + var errType string + + switch err := err.(type) { + case *Exception: + if o, ok := err.Value().(*Object); ok { + if c := o.Get("constructor"); c != nil { + if c, ok := c.(*Object); ok { + errType = c.Get("name").String() + } else { + t.Fatalf("%s: error constructor is not an object (%v)", name, o) + } + } else { + t.Fatalf("%s: error does not have a constructor (%v)", name, o) + } + } else { + t.Fatalf("%s: error is not an object (%v)", name, err.Value()) + } + case *CompilerSyntaxError: + errType = "SyntaxError" + case *CompilerReferenceError: + errType = "ReferenceError" + default: + t.Fatalf("%s: error is not a JS error: %v", name, err) + } + + if errType != meta.Negative.Type { + vm.vm.prg.dumpCode(t.Logf) + t.Fatalf("%s: unexpected error type (%s), expected (%s)", name, errType, meta.Negative.Type) + } + } + } else { + if meta.Negative.Type != "" { + vm.vm.prg.dumpCode(t.Logf) + t.Fatalf("%s: Expected error: %v", name, err) + } + } + + if vm.vm.sp != 0 { + t.Fatalf("sp: %d", vm.vm.sp) + } + + if l := len(vm.vm.iterStack); l > 0 { + t.Fatalf("iter stack is not empty: %d", l) + } + if async { + complete := false + for _, line := range out { + if strings.HasPrefix(line, "Test262:AsyncTestFailure:") { + t.Fatal(line) + } else if line == "Test262:AsyncTestComplete" { + complete = true + } + } + if !complete { + for _, line := range out { + t.Log(line) + } + t.Fatal("Test262:AsyncTestComplete was not printed") + } + } +} + +func (ctx *tc39TestCtx) runTC39File(name string, t testing.TB) { + if skipList[name] { + t.Skip("Excluded") + } + if skipPrefixes.Match(name) { + t.Skip("Excluded") + } + p := path.Join(ctx.base, name) + meta, src, err := parseTC39File(p) + if err != nil { + //t.Fatalf("Could not parse %s: %v", name, err) + t.Errorf("Could not parse %s: %v", name, err) + return + } + if meta.hasFlag("module") { + t.Skip("module") + } + if meta.Es5id == "" { + for _, feature := range meta.Features { + for _, bl := range featuresBlackList { + if feature == bl { + t.Skip("Blacklisted feature") + } + } + } + } + + var startTime time.Time + if ctx.enableBench { + startTime = time.Now() + } + + hasRaw := meta.hasFlag("raw") + + if hasRaw || !meta.hasFlag("onlyStrict") { + //log.Printf("Running normal test: %s", name) + t.Logf("Running normal test: %s", name) + ctx.runTC39Test(name, src, meta, t) + } + + if !hasRaw && !meta.hasFlag("noStrict") { + //log.Printf("Running strict test: %s", name) + t.Logf("Running strict test: %s", name) + ctx.runTC39Test(name, "'use strict';\n"+src, meta, t) + } + + if ctx.enableBench { + ctx.benchLock.Lock() + ctx.benchmark = append(ctx.benchmark, tc39BenchmarkItem{ + name: name, + duration: time.Since(startTime), + }) + ctx.benchLock.Unlock() + } + +} + +func (ctx *tc39TestCtx) init() { + ctx.prgCache = make(map[string]*Program) + ctx.sabStub = MustCompile("sabStub.js", ` + Object.defineProperty(this, "SharedArrayBuffer", { + get: function() { + throw IgnorableTestError; + } + });`, + false) +} + +func (ctx *tc39TestCtx) compile(base, name string) (*Program, error) { + ctx.prgCacheLock.Lock() + defer ctx.prgCacheLock.Unlock() + + prg := ctx.prgCache[name] + if prg == nil { + fname := path.Join(base, name) + f, err := os.Open(fname) + if err != nil { + return nil, err + } + defer f.Close() + + b, err := io.ReadAll(f) + if err != nil { + return nil, err + } + + str := string(b) + prg, err = Compile(name, str, false) + if err != nil { + return nil, err + } + ctx.prgCache[name] = prg + } + + return prg, nil +} + +func (ctx *tc39TestCtx) runFile(base, name string, vm *Runtime) error { + prg, err := ctx.compile(base, name) + if err != nil { + return err + } + _, err = vm.RunProgram(prg) + return err +} + +func (ctx *tc39TestCtx) runTC39Script(name, src string, includes []string, vm *Runtime) (err error, early bool) { + early = true + err = ctx.runFile(ctx.base, path.Join("harness", "assert.js"), vm) + if err != nil { + return + } + + err = ctx.runFile(ctx.base, path.Join("harness", "sta.js"), vm) + if err != nil { + return + } + + for _, include := range includes { + err = ctx.runFile(ctx.base, path.Join("harness", include), vm) + if err != nil { + return + } + } + + var p *Program + p, err = Compile(name, src, false) + + if err != nil { + return + } + + early = false + _, err = vm.RunProgram(p) + + return +} + +func (ctx *tc39TestCtx) runTC39Tests(name string) { + files, err := os.ReadDir(path.Join(ctx.base, name)) + if err != nil { + ctx.t.Fatal(err) + } + + for _, file := range files { + if file.Name()[0] == '.' { + continue + } + if file.IsDir() { + ctx.runTC39Tests(path.Join(name, file.Name())) + } else { + fileName := file.Name() + if strings.HasSuffix(fileName, ".js") && !strings.HasSuffix(fileName, "_FIXTURE.js") { + name := path.Join(name, fileName) + ctx.runTest(name, func(t *testing.T) { + ctx.runTC39File(name, t) + }) + } + } + } + +} + +func TestTC39(t *testing.T) { + if testing.Short() { + t.Skip() + } + + if _, err := os.Stat(tc39BASE); err != nil { + t.Skipf("If you want to run tc39 tests, download them from https://github.com/tc39/test262 and put into %s. See .tc39_test262_checkout.sh for the latest working commit id. (%v)", tc39BASE, err) + } + + ctx := &tc39TestCtx{ + base: tc39BASE, + } + ctx.init() + //ctx.enableBench = true + + t.Run("tc39", func(t *testing.T) { + ctx.t = t + //ctx.runTC39File("test/language/types/number/8.5.1.js", t) + ctx.runTC39Tests("test/language") + ctx.runTC39Tests("test/built-ins") + ctx.runTC39Tests("test/annexB/built-ins/String/prototype/substr") + ctx.runTC39Tests("test/annexB/built-ins/String/prototype/trimLeft") + ctx.runTC39Tests("test/annexB/built-ins/String/prototype/trimRight") + ctx.runTC39Tests("test/annexB/built-ins/escape") + ctx.runTC39Tests("test/annexB/built-ins/unescape") + ctx.runTC39Tests("test/annexB/built-ins/RegExp") + + ctx.flush() + }) + + if ctx.enableBench { + sort.Slice(ctx.benchmark, func(i, j int) bool { + return ctx.benchmark[i].duration > ctx.benchmark[j].duration + }) + bench := ctx.benchmark + if len(bench) > 50 { + bench = bench[:50] + } + for _, item := range bench { + fmt.Printf("%s\t%d\n", item.name, item.duration/time.Millisecond) + } + } +} diff --git a/goja/testdata/S15.10.2.12_A1_T1.js b/goja/testdata/S15.10.2.12_A1_T1.js new file mode 100644 index 0000000..e5e641d --- /dev/null +++ b/goja/testdata/S15.10.2.12_A1_T1.js @@ -0,0 +1,529 @@ +// Copyright 2009 the Sputnik authors. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +info: > + The production CharacterClassEscape :: s evaluates by returning the set of characters + containing the characters that are on the right-hand side of the WhiteSpace (7.2) or LineTerminator (7.3) productions +es5id: 15.10.2.12_A1_T1 +description: WhiteSpace +---*/ + +var i0 = ""; +for (var j = 0; j < 1024; j++) + i0 += String.fromCharCode(j); +var o0 = "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u000E\u000F\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001A\u001B\u001C\u001D\u001E\u001F\u0021\u0022\u0023\u0024\u0025\u0026\u0027\u0028\u0029\u002A\u002B\u002C\u002D\u002E\u002F\u0030\u0031\u0032\u0033\u0034\u0035\u0036\u0037\u0038\u0039\u003A\u003B\u003C\u003D\u003E\u003F\u0040\u0041\u0042\u0043\u0044\u0045\u0046\u0047\u0048\u0049\u004A\u004B\u004C\u004D\u004E\u004F\u0050\u0051\u0052\u0053\u0054\u0055\u0056\u0057\u0058\u0059\u005A\u005B\u005C\u005D\u005E\u005F\u0060\u0061\u0062\u0063\u0064\u0065\u0066\u0067\u0068\u0069\u006A\u006B\u006C\u006D\u006E\u006F\u0070\u0071\u0072\u0073\u0074\u0075\u0076\u0077\u0078\u0079\u007A\u007B\u007C\u007D\u007E\u007F\u0080\u0081\u0082\u0083\u0084\u0085\u0086\u0087\u0088\u0089\u008A\u008B\u008C\u008D\u008E\u008F\u0090\u0091\u0092\u0093\u0094\u0095\u0096\u0097\u0098\u0099\u009A\u009B\u009C\u009D\u009E\u009F\u00A1\u00A2\u00A3\u00A4\u00A5\u00A6\u00A7\u00A8\u00A9\u00AA\u00AB\u00AC\u00AD\u00AE\u00AF\u00B0\u00B1\u00B2\u00B3\u00B4\u00B5\u00B6\u00B7\u00B8\u00B9\u00BA\u00BB\u00BC\u00BD\u00BE\u00BF\u00C0\u00C1\u00C2\u00C3\u00C4\u00C5\u00C6\u00C7\u00C8\u00C9\u00CA\u00CB\u00CC\u00CD\u00CE\u00CF\u00D0\u00D1\u00D2\u00D3\u00D4\u00D5\u00D6\u00D7\u00D8\u00D9\u00DA\u00DB\u00DC\u00DD\u00DE\u00DF\u00E0\u00E1\u00E2\u00E3\u00E4\u00E5\u00E6\u00E7\u00E8\u00E9\u00EA\u00EB\u00EC\u00ED\u00EE\u00EF\u00F0\u00F1\u00F2\u00F3\u00F4\u00F5\u00F6\u00F7\u00F8\u00F9\u00FA\u00FB\u00FC\u00FD\u00FE\u00FF\u0100\u0101\u0102\u0103\u0104\u0105\u0106\u0107\u0108\u0109\u010A\u010B\u010C\u010D\u010E\u010F\u0110\u0111\u0112\u0113\u0114\u0115\u0116\u0117\u0118\u0119\u011A\u011B\u011C\u011D\u011E\u011F\u0120\u0121\u0122\u0123\u0124\u0125\u0126\u0127\u0128\u0129\u012A\u012B\u012C\u012D\u012E\u012F\u0130\u0131\u0132\u0133\u0134\u0135\u0136\u0137\u0138\u0139\u013A\u013B\u013C\u013D\u013E\u013F\u0140\u0141\u0142\u0143\u0144\u0145\u0146\u0147\u0148\u0149\u014A\u014B\u014C\u014D\u014E\u014F\u0150\u0151\u0152\u0153\u0154\u0155\u0156\u0157\u0158\u0159\u015A\u015B\u015C\u015D\u015E\u015F\u0160\u0161\u0162\u0163\u0164\u0165\u0166\u0167\u0168\u0169\u016A\u016B\u016C\u016D\u016E\u016F\u0170\u0171\u0172\u0173\u0174\u0175\u0176\u0177\u0178\u0179\u017A\u017B\u017C\u017D\u017E\u017F\u0180\u0181\u0182\u0183\u0184\u0185\u0186\u0187\u0188\u0189\u018A\u018B\u018C\u018D\u018E\u018F\u0190\u0191\u0192\u0193\u0194\u0195\u0196\u0197\u0198\u0199\u019A\u019B\u019C\u019D\u019E\u019F\u01A0\u01A1\u01A2\u01A3\u01A4\u01A5\u01A6\u01A7\u01A8\u01A9\u01AA\u01AB\u01AC\u01AD\u01AE\u01AF\u01B0\u01B1\u01B2\u01B3\u01B4\u01B5\u01B6\u01B7\u01B8\u01B9\u01BA\u01BB\u01BC\u01BD\u01BE\u01BF\u01C0\u01C1\u01C2\u01C3\u01C4\u01C5\u01C6\u01C7\u01C8\u01C9\u01CA\u01CB\u01CC\u01CD\u01CE\u01CF\u01D0\u01D1\u01D2\u01D3\u01D4\u01D5\u01D6\u01D7\u01D8\u01D9\u01DA\u01DB\u01DC\u01DD\u01DE\u01DF\u01E0\u01E1\u01E2\u01E3\u01E4\u01E5\u01E6\u01E7\u01E8\u01E9\u01EA\u01EB\u01EC\u01ED\u01EE\u01EF\u01F0\u01F1\u01F2\u01F3\u01F4\u01F5\u01F6\u01F7\u01F8\u01F9\u01FA\u01FB\u01FC\u01FD\u01FE\u01FF\u0200\u0201\u0202\u0203\u0204\u0205\u0206\u0207\u0208\u0209\u020A\u020B\u020C\u020D\u020E\u020F\u0210\u0211\u0212\u0213\u0214\u0215\u0216\u0217\u0218\u0219\u021A\u021B\u021C\u021D\u021E\u021F\u0220\u0221\u0222\u0223\u0224\u0225\u0226\u0227\u0228\u0229\u022A\u022B\u022C\u022D\u022E\u022F\u0230\u0231\u0232\u0233\u0234\u0235\u0236\u0237\u0238\u0239\u023A\u023B\u023C\u023D\u023E\u023F\u0240\u0241\u0242\u0243\u0244\u0245\u0246\u0247\u0248\u0249\u024A\u024B\u024C\u024D\u024E\u024F\u0250\u0251\u0252\u0253\u0254\u0255\u0256\u0257\u0258\u0259\u025A\u025B\u025C\u025D\u025E\u025F\u0260\u0261\u0262\u0263\u0264\u0265\u0266\u0267\u0268\u0269\u026A\u026B\u026C\u026D\u026E\u026F\u0270\u0271\u0272\u0273\u0274\u0275\u0276\u0277\u0278\u0279\u027A\u027B\u027C\u027D\u027E\u027F\u0280\u0281\u0282\u0283\u0284\u0285\u0286\u0287\u0288\u0289\u028A\u028B\u028C\u028D\u028E\u028F\u0290\u0291\u0292\u0293\u0294\u0295\u0296\u0297\u0298\u0299\u029A\u029B\u029C\u029D\u029E\u029F\u02A0\u02A1\u02A2\u02A3\u02A4\u02A5\u02A6\u02A7\u02A8\u02A9\u02AA\u02AB\u02AC\u02AD\u02AE\u02AF\u02B0\u02B1\u02B2\u02B3\u02B4\u02B5\u02B6\u02B7\u02B8\u02B9\u02BA\u02BB\u02BC\u02BD\u02BE\u02BF\u02C0\u02C1\u02C2\u02C3\u02C4\u02C5\u02C6\u02C7\u02C8\u02C9\u02CA\u02CB\u02CC\u02CD\u02CE\u02CF\u02D0\u02D1\u02D2\u02D3\u02D4\u02D5\u02D6\u02D7\u02D8\u02D9\u02DA\u02DB\u02DC\u02DD\u02DE\u02DF\u02E0\u02E1\u02E2\u02E3\u02E4\u02E5\u02E6\u02E7\u02E8\u02E9\u02EA\u02EB\u02EC\u02ED\u02EE\u02EF\u02F0\u02F1\u02F2\u02F3\u02F4\u02F5\u02F6\u02F7\u02F8\u02F9\u02FA\u02FB\u02FC\u02FD\u02FE\u02FF\u0300\u0301\u0302\u0303\u0304\u0305\u0306\u0307\u0308\u0309\u030A\u030B\u030C\u030D\u030E\u030F\u0310\u0311\u0312\u0313\u0314\u0315\u0316\u0317\u0318\u0319\u031A\u031B\u031C\u031D\u031E\u031F\u0320\u0321\u0322\u0323\u0324\u0325\u0326\u0327\u0328\u0329\u032A\u032B\u032C\u032D\u032E\u032F\u0330\u0331\u0332\u0333\u0334\u0335\u0336\u0337\u0338\u0339\u033A\u033B\u033C\u033D\u033E\u033F\u0340\u0341\u0342\u0343\u0344\u0345\u0346\u0347\u0348\u0349\u034A\u034B\u034C\u034D\u034E\u034F\u0350\u0351\u0352\u0353\u0354\u0355\u0356\u0357\u0358\u0359\u035A\u035B\u035C\u035D\u035E\u035F\u0360\u0361\u0362\u0363\u0364\u0365\u0366\u0367\u0368\u0369\u036A\u036B\u036C\u036D\u036E\u036F\u0370\u0371\u0372\u0373\u0374\u0375\u0376\u0377\u0378\u0379\u037A\u037B\u037C\u037D\u037E\u037F\u0380\u0381\u0382\u0383\u0384\u0385\u0386\u0387\u0388\u0389\u038A\u038B\u038C\u038D\u038E\u038F\u0390\u0391\u0392\u0393\u0394\u0395\u0396\u0397\u0398\u0399\u039A\u039B\u039C\u039D\u039E\u039F\u03A0\u03A1\u03A2\u03A3\u03A4\u03A5\u03A6\u03A7\u03A8\u03A9\u03AA\u03AB\u03AC\u03AD\u03AE\u03AF\u03B0\u03B1\u03B2\u03B3\u03B4\u03B5\u03B6\u03B7\u03B8\u03B9\u03BA\u03BB\u03BC\u03BD\u03BE\u03BF\u03C0\u03C1\u03C2\u03C3\u03C4\u03C5\u03C6\u03C7\u03C8\u03C9\u03CA\u03CB\u03CC\u03CD\u03CE\u03CF\u03D0\u03D1\u03D2\u03D3\u03D4\u03D5\u03D6\u03D7\u03D8\u03D9\u03DA\u03DB\u03DC\u03DD\u03DE\u03DF\u03E0\u03E1\u03E2\u03E3\u03E4\u03E5\u03E6\u03E7\u03E8\u03E9\u03EA\u03EB\u03EC\u03ED\u03EE\u03EF\u03F0\u03F1\u03F2\u03F3\u03F4\u03F5\u03F6\u03F7\u03F8\u03F9\u03FA\u03FB\u03FC\u03FD\u03FE\u03FF"; +if (i0.replace(/\s+/g, "") !== o0) { + $ERROR("#0: Error matching character class \s between character 0 and 3ff"); +} + +var i1 = ""; +for (var j = 1024; j < 2048; j++) + i1 += String.fromCharCode(j); +var o1 = i1; +if (i1.replace(/\s+/g, "") !== o1) { + $ERROR("#1: Error matching character class \s between character 400 and 7ff"); +} + +var i2 = ""; +for (var j = 2048; j < 3072; j++) + i2 += String.fromCharCode(j); +var o2 = i2; +if (i2.replace(/\s+/g, "") !== o2) { + $ERROR("#2: Error matching character class \s between character 800 and bff"); +} + +var i3 = ""; +for (var j = 3072; j < 4096; j++) + i3 += String.fromCharCode(j); +var o3 = i3; +if (i3.replace(/\s+/g, "") !== o3) { + $ERROR("#3: Error matching character class \s between character c00 and fff"); +} + +var i4 = ""; +for (var j = 4096; j < 5120; j++) + i4 += String.fromCharCode(j); +var o4 = i4; +if (i4.replace(/\s+/g, "") !== o4) { + $ERROR("#4: Error matching character class \s between character 1000 and 13ff"); +} + +var i5 = ""; +for (var j = 5120; j < 6144; j++) + i5 += String.fromCharCode(j); +var o5 = "\u1400\u1401\u1402\u1403\u1404\u1405\u1406\u1407\u1408\u1409\u140A\u140B\u140C\u140D\u140E\u140F\u1410\u1411\u1412\u1413\u1414\u1415\u1416\u1417\u1418\u1419\u141A\u141B\u141C\u141D\u141E\u141F\u1420\u1421\u1422\u1423\u1424\u1425\u1426\u1427\u1428\u1429\u142A\u142B\u142C\u142D\u142E\u142F\u1430\u1431\u1432\u1433\u1434\u1435\u1436\u1437\u1438\u1439\u143A\u143B\u143C\u143D\u143E\u143F\u1440\u1441\u1442\u1443\u1444\u1445\u1446\u1447\u1448\u1449\u144A\u144B\u144C\u144D\u144E\u144F\u1450\u1451\u1452\u1453\u1454\u1455\u1456\u1457\u1458\u1459\u145A\u145B\u145C\u145D\u145E\u145F\u1460\u1461\u1462\u1463\u1464\u1465\u1466\u1467\u1468\u1469\u146A\u146B\u146C\u146D\u146E\u146F\u1470\u1471\u1472\u1473\u1474\u1475\u1476\u1477\u1478\u1479\u147A\u147B\u147C\u147D\u147E\u147F\u1480\u1481\u1482\u1483\u1484\u1485\u1486\u1487\u1488\u1489\u148A\u148B\u148C\u148D\u148E\u148F\u1490\u1491\u1492\u1493\u1494\u1495\u1496\u1497\u1498\u1499\u149A\u149B\u149C\u149D\u149E\u149F\u14A0\u14A1\u14A2\u14A3\u14A4\u14A5\u14A6\u14A7\u14A8\u14A9\u14AA\u14AB\u14AC\u14AD\u14AE\u14AF\u14B0\u14B1\u14B2\u14B3\u14B4\u14B5\u14B6\u14B7\u14B8\u14B9\u14BA\u14BB\u14BC\u14BD\u14BE\u14BF\u14C0\u14C1\u14C2\u14C3\u14C4\u14C5\u14C6\u14C7\u14C8\u14C9\u14CA\u14CB\u14CC\u14CD\u14CE\u14CF\u14D0\u14D1\u14D2\u14D3\u14D4\u14D5\u14D6\u14D7\u14D8\u14D9\u14DA\u14DB\u14DC\u14DD\u14DE\u14DF\u14E0\u14E1\u14E2\u14E3\u14E4\u14E5\u14E6\u14E7\u14E8\u14E9\u14EA\u14EB\u14EC\u14ED\u14EE\u14EF\u14F0\u14F1\u14F2\u14F3\u14F4\u14F5\u14F6\u14F7\u14F8\u14F9\u14FA\u14FB\u14FC\u14FD\u14FE\u14FF\u1500\u1501\u1502\u1503\u1504\u1505\u1506\u1507\u1508\u1509\u150A\u150B\u150C\u150D\u150E\u150F\u1510\u1511\u1512\u1513\u1514\u1515\u1516\u1517\u1518\u1519\u151A\u151B\u151C\u151D\u151E\u151F\u1520\u1521\u1522\u1523\u1524\u1525\u1526\u1527\u1528\u1529\u152A\u152B\u152C\u152D\u152E\u152F\u1530\u1531\u1532\u1533\u1534\u1535\u1536\u1537\u1538\u1539\u153A\u153B\u153C\u153D\u153E\u153F\u1540\u1541\u1542\u1543\u1544\u1545\u1546\u1547\u1548\u1549\u154A\u154B\u154C\u154D\u154E\u154F\u1550\u1551\u1552\u1553\u1554\u1555\u1556\u1557\u1558\u1559\u155A\u155B\u155C\u155D\u155E\u155F\u1560\u1561\u1562\u1563\u1564\u1565\u1566\u1567\u1568\u1569\u156A\u156B\u156C\u156D\u156E\u156F\u1570\u1571\u1572\u1573\u1574\u1575\u1576\u1577\u1578\u1579\u157A\u157B\u157C\u157D\u157E\u157F\u1580\u1581\u1582\u1583\u1584\u1585\u1586\u1587\u1588\u1589\u158A\u158B\u158C\u158D\u158E\u158F\u1590\u1591\u1592\u1593\u1594\u1595\u1596\u1597\u1598\u1599\u159A\u159B\u159C\u159D\u159E\u159F\u15A0\u15A1\u15A2\u15A3\u15A4\u15A5\u15A6\u15A7\u15A8\u15A9\u15AA\u15AB\u15AC\u15AD\u15AE\u15AF\u15B0\u15B1\u15B2\u15B3\u15B4\u15B5\u15B6\u15B7\u15B8\u15B9\u15BA\u15BB\u15BC\u15BD\u15BE\u15BF\u15C0\u15C1\u15C2\u15C3\u15C4\u15C5\u15C6\u15C7\u15C8\u15C9\u15CA\u15CB\u15CC\u15CD\u15CE\u15CF\u15D0\u15D1\u15D2\u15D3\u15D4\u15D5\u15D6\u15D7\u15D8\u15D9\u15DA\u15DB\u15DC\u15DD\u15DE\u15DF\u15E0\u15E1\u15E2\u15E3\u15E4\u15E5\u15E6\u15E7\u15E8\u15E9\u15EA\u15EB\u15EC\u15ED\u15EE\u15EF\u15F0\u15F1\u15F2\u15F3\u15F4\u15F5\u15F6\u15F7\u15F8\u15F9\u15FA\u15FB\u15FC\u15FD\u15FE\u15FF\u1600\u1601\u1602\u1603\u1604\u1605\u1606\u1607\u1608\u1609\u160A\u160B\u160C\u160D\u160E\u160F\u1610\u1611\u1612\u1613\u1614\u1615\u1616\u1617\u1618\u1619\u161A\u161B\u161C\u161D\u161E\u161F\u1620\u1621\u1622\u1623\u1624\u1625\u1626\u1627\u1628\u1629\u162A\u162B\u162C\u162D\u162E\u162F\u1630\u1631\u1632\u1633\u1634\u1635\u1636\u1637\u1638\u1639\u163A\u163B\u163C\u163D\u163E\u163F\u1640\u1641\u1642\u1643\u1644\u1645\u1646\u1647\u1648\u1649\u164A\u164B\u164C\u164D\u164E\u164F\u1650\u1651\u1652\u1653\u1654\u1655\u1656\u1657\u1658\u1659\u165A\u165B\u165C\u165D\u165E\u165F\u1660\u1661\u1662\u1663\u1664\u1665\u1666\u1667\u1668\u1669\u166A\u166B\u166C\u166D\u166E\u166F\u1670\u1671\u1672\u1673\u1674\u1675\u1676\u1677\u1678\u1679\u167A\u167B\u167C\u167D\u167E\u167F\u1681\u1682\u1683\u1684\u1685\u1686\u1687\u1688\u1689\u168A\u168B\u168C\u168D\u168E\u168F\u1690\u1691\u1692\u1693\u1694\u1695\u1696\u1697\u1698\u1699\u169A\u169B\u169C\u169D\u169E\u169F\u16A0\u16A1\u16A2\u16A3\u16A4\u16A5\u16A6\u16A7\u16A8\u16A9\u16AA\u16AB\u16AC\u16AD\u16AE\u16AF\u16B0\u16B1\u16B2\u16B3\u16B4\u16B5\u16B6\u16B7\u16B8\u16B9\u16BA\u16BB\u16BC\u16BD\u16BE\u16BF\u16C0\u16C1\u16C2\u16C3\u16C4\u16C5\u16C6\u16C7\u16C8\u16C9\u16CA\u16CB\u16CC\u16CD\u16CE\u16CF\u16D0\u16D1\u16D2\u16D3\u16D4\u16D5\u16D6\u16D7\u16D8\u16D9\u16DA\u16DB\u16DC\u16DD\u16DE\u16DF\u16E0\u16E1\u16E2\u16E3\u16E4\u16E5\u16E6\u16E7\u16E8\u16E9\u16EA\u16EB\u16EC\u16ED\u16EE\u16EF\u16F0\u16F1\u16F2\u16F3\u16F4\u16F5\u16F6\u16F7\u16F8\u16F9\u16FA\u16FB\u16FC\u16FD\u16FE\u16FF\u1700\u1701\u1702\u1703\u1704\u1705\u1706\u1707\u1708\u1709\u170A\u170B\u170C\u170D\u170E\u170F\u1710\u1711\u1712\u1713\u1714\u1715\u1716\u1717\u1718\u1719\u171A\u171B\u171C\u171D\u171E\u171F\u1720\u1721\u1722\u1723\u1724\u1725\u1726\u1727\u1728\u1729\u172A\u172B\u172C\u172D\u172E\u172F\u1730\u1731\u1732\u1733\u1734\u1735\u1736\u1737\u1738\u1739\u173A\u173B\u173C\u173D\u173E\u173F\u1740\u1741\u1742\u1743\u1744\u1745\u1746\u1747\u1748\u1749\u174A\u174B\u174C\u174D\u174E\u174F\u1750\u1751\u1752\u1753\u1754\u1755\u1756\u1757\u1758\u1759\u175A\u175B\u175C\u175D\u175E\u175F\u1760\u1761\u1762\u1763\u1764\u1765\u1766\u1767\u1768\u1769\u176A\u176B\u176C\u176D\u176E\u176F\u1770\u1771\u1772\u1773\u1774\u1775\u1776\u1777\u1778\u1779\u177A\u177B\u177C\u177D\u177E\u177F\u1780\u1781\u1782\u1783\u1784\u1785\u1786\u1787\u1788\u1789\u178A\u178B\u178C\u178D\u178E\u178F\u1790\u1791\u1792\u1793\u1794\u1795\u1796\u1797\u1798\u1799\u179A\u179B\u179C\u179D\u179E\u179F\u17A0\u17A1\u17A2\u17A3\u17A4\u17A5\u17A6\u17A7\u17A8\u17A9\u17AA\u17AB\u17AC\u17AD\u17AE\u17AF\u17B0\u17B1\u17B2\u17B3\u17B4\u17B5\u17B6\u17B7\u17B8\u17B9\u17BA\u17BB\u17BC\u17BD\u17BE\u17BF\u17C0\u17C1\u17C2\u17C3\u17C4\u17C5\u17C6\u17C7\u17C8\u17C9\u17CA\u17CB\u17CC\u17CD\u17CE\u17CF\u17D0\u17D1\u17D2\u17D3\u17D4\u17D5\u17D6\u17D7\u17D8\u17D9\u17DA\u17DB\u17DC\u17DD\u17DE\u17DF\u17E0\u17E1\u17E2\u17E3\u17E4\u17E5\u17E6\u17E7\u17E8\u17E9\u17EA\u17EB\u17EC\u17ED\u17EE\u17EF\u17F0\u17F1\u17F2\u17F3\u17F4\u17F5\u17F6\u17F7\u17F8\u17F9\u17FA\u17FB\u17FC\u17FD\u17FE\u17FF"; +if (i5.replace(/\s+/g, "") !== o5) { + $ERROR("#5: Error matching character class \s between character 1400 and 17ff"); +} + +var i6 = ""; +for (var j = 6144; j < 7168; j++) + i6 += String.fromCharCode(j); +var o6 = "\u1800\u1801\u1802\u1803\u1804\u1805\u1806\u1807\u1808\u1809\u180A\u180B\u180C\u180D\u180E\u180F\u1810\u1811\u1812\u1813\u1814\u1815\u1816\u1817\u1818\u1819\u181A\u181B\u181C\u181D\u181E\u181F\u1820\u1821\u1822\u1823\u1824\u1825\u1826\u1827\u1828\u1829\u182A\u182B\u182C\u182D\u182E\u182F\u1830\u1831\u1832\u1833\u1834\u1835\u1836\u1837\u1838\u1839\u183A\u183B\u183C\u183D\u183E\u183F\u1840\u1841\u1842\u1843\u1844\u1845\u1846\u1847\u1848\u1849\u184A\u184B\u184C\u184D\u184E\u184F\u1850\u1851\u1852\u1853\u1854\u1855\u1856\u1857\u1858\u1859\u185A\u185B\u185C\u185D\u185E\u185F\u1860\u1861\u1862\u1863\u1864\u1865\u1866\u1867\u1868\u1869\u186A\u186B\u186C\u186D\u186E\u186F\u1870\u1871\u1872\u1873\u1874\u1875\u1876\u1877\u1878\u1879\u187A\u187B\u187C\u187D\u187E\u187F\u1880\u1881\u1882\u1883\u1884\u1885\u1886\u1887\u1888\u1889\u188A\u188B\u188C\u188D\u188E\u188F\u1890\u1891\u1892\u1893\u1894\u1895\u1896\u1897\u1898\u1899\u189A\u189B\u189C\u189D\u189E\u189F\u18A0\u18A1\u18A2\u18A3\u18A4\u18A5\u18A6\u18A7\u18A8\u18A9\u18AA\u18AB\u18AC\u18AD\u18AE\u18AF\u18B0\u18B1\u18B2\u18B3\u18B4\u18B5\u18B6\u18B7\u18B8\u18B9\u18BA\u18BB\u18BC\u18BD\u18BE\u18BF\u18C0\u18C1\u18C2\u18C3\u18C4\u18C5\u18C6\u18C7\u18C8\u18C9\u18CA\u18CB\u18CC\u18CD\u18CE\u18CF\u18D0\u18D1\u18D2\u18D3\u18D4\u18D5\u18D6\u18D7\u18D8\u18D9\u18DA\u18DB\u18DC\u18DD\u18DE\u18DF\u18E0\u18E1\u18E2\u18E3\u18E4\u18E5\u18E6\u18E7\u18E8\u18E9\u18EA\u18EB\u18EC\u18ED\u18EE\u18EF\u18F0\u18F1\u18F2\u18F3\u18F4\u18F5\u18F6\u18F7\u18F8\u18F9\u18FA\u18FB\u18FC\u18FD\u18FE\u18FF\u1900\u1901\u1902\u1903\u1904\u1905\u1906\u1907\u1908\u1909\u190A\u190B\u190C\u190D\u190E\u190F\u1910\u1911\u1912\u1913\u1914\u1915\u1916\u1917\u1918\u1919\u191A\u191B\u191C\u191D\u191E\u191F\u1920\u1921\u1922\u1923\u1924\u1925\u1926\u1927\u1928\u1929\u192A\u192B\u192C\u192D\u192E\u192F\u1930\u1931\u1932\u1933\u1934\u1935\u1936\u1937\u1938\u1939\u193A\u193B\u193C\u193D\u193E\u193F\u1940\u1941\u1942\u1943\u1944\u1945\u1946\u1947\u1948\u1949\u194A\u194B\u194C\u194D\u194E\u194F\u1950\u1951\u1952\u1953\u1954\u1955\u1956\u1957\u1958\u1959\u195A\u195B\u195C\u195D\u195E\u195F\u1960\u1961\u1962\u1963\u1964\u1965\u1966\u1967\u1968\u1969\u196A\u196B\u196C\u196D\u196E\u196F\u1970\u1971\u1972\u1973\u1974\u1975\u1976\u1977\u1978\u1979\u197A\u197B\u197C\u197D\u197E\u197F\u1980\u1981\u1982\u1983\u1984\u1985\u1986\u1987\u1988\u1989\u198A\u198B\u198C\u198D\u198E\u198F\u1990\u1991\u1992\u1993\u1994\u1995\u1996\u1997\u1998\u1999\u199A\u199B\u199C\u199D\u199E\u199F\u19A0\u19A1\u19A2\u19A3\u19A4\u19A5\u19A6\u19A7\u19A8\u19A9\u19AA\u19AB\u19AC\u19AD\u19AE\u19AF\u19B0\u19B1\u19B2\u19B3\u19B4\u19B5\u19B6\u19B7\u19B8\u19B9\u19BA\u19BB\u19BC\u19BD\u19BE\u19BF\u19C0\u19C1\u19C2\u19C3\u19C4\u19C5\u19C6\u19C7\u19C8\u19C9\u19CA\u19CB\u19CC\u19CD\u19CE\u19CF\u19D0\u19D1\u19D2\u19D3\u19D4\u19D5\u19D6\u19D7\u19D8\u19D9\u19DA\u19DB\u19DC\u19DD\u19DE\u19DF\u19E0\u19E1\u19E2\u19E3\u19E4\u19E5\u19E6\u19E7\u19E8\u19E9\u19EA\u19EB\u19EC\u19ED\u19EE\u19EF\u19F0\u19F1\u19F2\u19F3\u19F4\u19F5\u19F6\u19F7\u19F8\u19F9\u19FA\u19FB\u19FC\u19FD\u19FE\u19FF\u1A00\u1A01\u1A02\u1A03\u1A04\u1A05\u1A06\u1A07\u1A08\u1A09\u1A0A\u1A0B\u1A0C\u1A0D\u1A0E\u1A0F\u1A10\u1A11\u1A12\u1A13\u1A14\u1A15\u1A16\u1A17\u1A18\u1A19\u1A1A\u1A1B\u1A1C\u1A1D\u1A1E\u1A1F\u1A20\u1A21\u1A22\u1A23\u1A24\u1A25\u1A26\u1A27\u1A28\u1A29\u1A2A\u1A2B\u1A2C\u1A2D\u1A2E\u1A2F\u1A30\u1A31\u1A32\u1A33\u1A34\u1A35\u1A36\u1A37\u1A38\u1A39\u1A3A\u1A3B\u1A3C\u1A3D\u1A3E\u1A3F\u1A40\u1A41\u1A42\u1A43\u1A44\u1A45\u1A46\u1A47\u1A48\u1A49\u1A4A\u1A4B\u1A4C\u1A4D\u1A4E\u1A4F\u1A50\u1A51\u1A52\u1A53\u1A54\u1A55\u1A56\u1A57\u1A58\u1A59\u1A5A\u1A5B\u1A5C\u1A5D\u1A5E\u1A5F\u1A60\u1A61\u1A62\u1A63\u1A64\u1A65\u1A66\u1A67\u1A68\u1A69\u1A6A\u1A6B\u1A6C\u1A6D\u1A6E\u1A6F\u1A70\u1A71\u1A72\u1A73\u1A74\u1A75\u1A76\u1A77\u1A78\u1A79\u1A7A\u1A7B\u1A7C\u1A7D\u1A7E\u1A7F\u1A80\u1A81\u1A82\u1A83\u1A84\u1A85\u1A86\u1A87\u1A88\u1A89\u1A8A\u1A8B\u1A8C\u1A8D\u1A8E\u1A8F\u1A90\u1A91\u1A92\u1A93\u1A94\u1A95\u1A96\u1A97\u1A98\u1A99\u1A9A\u1A9B\u1A9C\u1A9D\u1A9E\u1A9F\u1AA0\u1AA1\u1AA2\u1AA3\u1AA4\u1AA5\u1AA6\u1AA7\u1AA8\u1AA9\u1AAA\u1AAB\u1AAC\u1AAD\u1AAE\u1AAF\u1AB0\u1AB1\u1AB2\u1AB3\u1AB4\u1AB5\u1AB6\u1AB7\u1AB8\u1AB9\u1ABA\u1ABB\u1ABC\u1ABD\u1ABE\u1ABF\u1AC0\u1AC1\u1AC2\u1AC3\u1AC4\u1AC5\u1AC6\u1AC7\u1AC8\u1AC9\u1ACA\u1ACB\u1ACC\u1ACD\u1ACE\u1ACF\u1AD0\u1AD1\u1AD2\u1AD3\u1AD4\u1AD5\u1AD6\u1AD7\u1AD8\u1AD9\u1ADA\u1ADB\u1ADC\u1ADD\u1ADE\u1ADF\u1AE0\u1AE1\u1AE2\u1AE3\u1AE4\u1AE5\u1AE6\u1AE7\u1AE8\u1AE9\u1AEA\u1AEB\u1AEC\u1AED\u1AEE\u1AEF\u1AF0\u1AF1\u1AF2\u1AF3\u1AF4\u1AF5\u1AF6\u1AF7\u1AF8\u1AF9\u1AFA\u1AFB\u1AFC\u1AFD\u1AFE\u1AFF\u1B00\u1B01\u1B02\u1B03\u1B04\u1B05\u1B06\u1B07\u1B08\u1B09\u1B0A\u1B0B\u1B0C\u1B0D\u1B0E\u1B0F\u1B10\u1B11\u1B12\u1B13\u1B14\u1B15\u1B16\u1B17\u1B18\u1B19\u1B1A\u1B1B\u1B1C\u1B1D\u1B1E\u1B1F\u1B20\u1B21\u1B22\u1B23\u1B24\u1B25\u1B26\u1B27\u1B28\u1B29\u1B2A\u1B2B\u1B2C\u1B2D\u1B2E\u1B2F\u1B30\u1B31\u1B32\u1B33\u1B34\u1B35\u1B36\u1B37\u1B38\u1B39\u1B3A\u1B3B\u1B3C\u1B3D\u1B3E\u1B3F\u1B40\u1B41\u1B42\u1B43\u1B44\u1B45\u1B46\u1B47\u1B48\u1B49\u1B4A\u1B4B\u1B4C\u1B4D\u1B4E\u1B4F\u1B50\u1B51\u1B52\u1B53\u1B54\u1B55\u1B56\u1B57\u1B58\u1B59\u1B5A\u1B5B\u1B5C\u1B5D\u1B5E\u1B5F\u1B60\u1B61\u1B62\u1B63\u1B64\u1B65\u1B66\u1B67\u1B68\u1B69\u1B6A\u1B6B\u1B6C\u1B6D\u1B6E\u1B6F\u1B70\u1B71\u1B72\u1B73\u1B74\u1B75\u1B76\u1B77\u1B78\u1B79\u1B7A\u1B7B\u1B7C\u1B7D\u1B7E\u1B7F\u1B80\u1B81\u1B82\u1B83\u1B84\u1B85\u1B86\u1B87\u1B88\u1B89\u1B8A\u1B8B\u1B8C\u1B8D\u1B8E\u1B8F\u1B90\u1B91\u1B92\u1B93\u1B94\u1B95\u1B96\u1B97\u1B98\u1B99\u1B9A\u1B9B\u1B9C\u1B9D\u1B9E\u1B9F\u1BA0\u1BA1\u1BA2\u1BA3\u1BA4\u1BA5\u1BA6\u1BA7\u1BA8\u1BA9\u1BAA\u1BAB\u1BAC\u1BAD\u1BAE\u1BAF\u1BB0\u1BB1\u1BB2\u1BB3\u1BB4\u1BB5\u1BB6\u1BB7\u1BB8\u1BB9\u1BBA\u1BBB\u1BBC\u1BBD\u1BBE\u1BBF\u1BC0\u1BC1\u1BC2\u1BC3\u1BC4\u1BC5\u1BC6\u1BC7\u1BC8\u1BC9\u1BCA\u1BCB\u1BCC\u1BCD\u1BCE\u1BCF\u1BD0\u1BD1\u1BD2\u1BD3\u1BD4\u1BD5\u1BD6\u1BD7\u1BD8\u1BD9\u1BDA\u1BDB\u1BDC\u1BDD\u1BDE\u1BDF\u1BE0\u1BE1\u1BE2\u1BE3\u1BE4\u1BE5\u1BE6\u1BE7\u1BE8\u1BE9\u1BEA\u1BEB\u1BEC\u1BED\u1BEE\u1BEF\u1BF0\u1BF1\u1BF2\u1BF3\u1BF4\u1BF5\u1BF6\u1BF7\u1BF8\u1BF9\u1BFA\u1BFB\u1BFC\u1BFD\u1BFE\u1BFF"; +if (i6.replace(/\s+/g, "") !== o6) { + $ERROR("#6: Error matching character class \s between character 1800 and 1bff"); +} + +var i7 = ""; +for (var j = 7168; j < 8192; j++) + i7 += String.fromCharCode(j); +var o7 = i7; +if (i7.replace(/\s+/g, "") !== o7) { + $ERROR("#7: Error matching character class \s between character 1c00 and 1fff"); +} + +var i8 = ""; +for (var j = 8192; j < 9216; j++) + i8 += String.fromCharCode(j); +var o8 = "\u200B\u200C\u200D\u200E\u200F\u2010\u2011\u2012\u2013\u2014\u2015\u2016\u2017\u2018\u2019\u201A\u201B\u201C\u201D\u201E\u201F\u2020\u2021\u2022\u2023\u2024\u2025\u2026\u2027\u202A\u202B\u202C\u202D\u202E\u2030\u2031\u2032\u2033\u2034\u2035\u2036\u2037\u2038\u2039\u203A\u203B\u203C\u203D\u203E\u203F\u2040\u2041\u2042\u2043\u2044\u2045\u2046\u2047\u2048\u2049\u204A\u204B\u204C\u204D\u204E\u204F\u2050\u2051\u2052\u2053\u2054\u2055\u2056\u2057\u2058\u2059\u205A\u205B\u205C\u205D\u205E\u2060\u2061\u2062\u2063\u2064\u2065\u2066\u2067\u2068\u2069\u206A\u206B\u206C\u206D\u206E\u206F\u2070\u2071\u2072\u2073\u2074\u2075\u2076\u2077\u2078\u2079\u207A\u207B\u207C\u207D\u207E\u207F\u2080\u2081\u2082\u2083\u2084\u2085\u2086\u2087\u2088\u2089\u208A\u208B\u208C\u208D\u208E\u208F\u2090\u2091\u2092\u2093\u2094\u2095\u2096\u2097\u2098\u2099\u209A\u209B\u209C\u209D\u209E\u209F\u20A0\u20A1\u20A2\u20A3\u20A4\u20A5\u20A6\u20A7\u20A8\u20A9\u20AA\u20AB\u20AC\u20AD\u20AE\u20AF\u20B0\u20B1\u20B2\u20B3\u20B4\u20B5\u20B6\u20B7\u20B8\u20B9\u20BA\u20BB\u20BC\u20BD\u20BE\u20BF\u20C0\u20C1\u20C2\u20C3\u20C4\u20C5\u20C6\u20C7\u20C8\u20C9\u20CA\u20CB\u20CC\u20CD\u20CE\u20CF\u20D0\u20D1\u20D2\u20D3\u20D4\u20D5\u20D6\u20D7\u20D8\u20D9\u20DA\u20DB\u20DC\u20DD\u20DE\u20DF\u20E0\u20E1\u20E2\u20E3\u20E4\u20E5\u20E6\u20E7\u20E8\u20E9\u20EA\u20EB\u20EC\u20ED\u20EE\u20EF\u20F0\u20F1\u20F2\u20F3\u20F4\u20F5\u20F6\u20F7\u20F8\u20F9\u20FA\u20FB\u20FC\u20FD\u20FE\u20FF\u2100\u2101\u2102\u2103\u2104\u2105\u2106\u2107\u2108\u2109\u210A\u210B\u210C\u210D\u210E\u210F\u2110\u2111\u2112\u2113\u2114\u2115\u2116\u2117\u2118\u2119\u211A\u211B\u211C\u211D\u211E\u211F\u2120\u2121\u2122\u2123\u2124\u2125\u2126\u2127\u2128\u2129\u212A\u212B\u212C\u212D\u212E\u212F\u2130\u2131\u2132\u2133\u2134\u2135\u2136\u2137\u2138\u2139\u213A\u213B\u213C\u213D\u213E\u213F\u2140\u2141\u2142\u2143\u2144\u2145\u2146\u2147\u2148\u2149\u214A\u214B\u214C\u214D\u214E\u214F\u2150\u2151\u2152\u2153\u2154\u2155\u2156\u2157\u2158\u2159\u215A\u215B\u215C\u215D\u215E\u215F\u2160\u2161\u2162\u2163\u2164\u2165\u2166\u2167\u2168\u2169\u216A\u216B\u216C\u216D\u216E\u216F\u2170\u2171\u2172\u2173\u2174\u2175\u2176\u2177\u2178\u2179\u217A\u217B\u217C\u217D\u217E\u217F\u2180\u2181\u2182\u2183\u2184\u2185\u2186\u2187\u2188\u2189\u218A\u218B\u218C\u218D\u218E\u218F\u2190\u2191\u2192\u2193\u2194\u2195\u2196\u2197\u2198\u2199\u219A\u219B\u219C\u219D\u219E\u219F\u21A0\u21A1\u21A2\u21A3\u21A4\u21A5\u21A6\u21A7\u21A8\u21A9\u21AA\u21AB\u21AC\u21AD\u21AE\u21AF\u21B0\u21B1\u21B2\u21B3\u21B4\u21B5\u21B6\u21B7\u21B8\u21B9\u21BA\u21BB\u21BC\u21BD\u21BE\u21BF\u21C0\u21C1\u21C2\u21C3\u21C4\u21C5\u21C6\u21C7\u21C8\u21C9\u21CA\u21CB\u21CC\u21CD\u21CE\u21CF\u21D0\u21D1\u21D2\u21D3\u21D4\u21D5\u21D6\u21D7\u21D8\u21D9\u21DA\u21DB\u21DC\u21DD\u21DE\u21DF\u21E0\u21E1\u21E2\u21E3\u21E4\u21E5\u21E6\u21E7\u21E8\u21E9\u21EA\u21EB\u21EC\u21ED\u21EE\u21EF\u21F0\u21F1\u21F2\u21F3\u21F4\u21F5\u21F6\u21F7\u21F8\u21F9\u21FA\u21FB\u21FC\u21FD\u21FE\u21FF\u2200\u2201\u2202\u2203\u2204\u2205\u2206\u2207\u2208\u2209\u220A\u220B\u220C\u220D\u220E\u220F\u2210\u2211\u2212\u2213\u2214\u2215\u2216\u2217\u2218\u2219\u221A\u221B\u221C\u221D\u221E\u221F\u2220\u2221\u2222\u2223\u2224\u2225\u2226\u2227\u2228\u2229\u222A\u222B\u222C\u222D\u222E\u222F\u2230\u2231\u2232\u2233\u2234\u2235\u2236\u2237\u2238\u2239\u223A\u223B\u223C\u223D\u223E\u223F\u2240\u2241\u2242\u2243\u2244\u2245\u2246\u2247\u2248\u2249\u224A\u224B\u224C\u224D\u224E\u224F\u2250\u2251\u2252\u2253\u2254\u2255\u2256\u2257\u2258\u2259\u225A\u225B\u225C\u225D\u225E\u225F\u2260\u2261\u2262\u2263\u2264\u2265\u2266\u2267\u2268\u2269\u226A\u226B\u226C\u226D\u226E\u226F\u2270\u2271\u2272\u2273\u2274\u2275\u2276\u2277\u2278\u2279\u227A\u227B\u227C\u227D\u227E\u227F\u2280\u2281\u2282\u2283\u2284\u2285\u2286\u2287\u2288\u2289\u228A\u228B\u228C\u228D\u228E\u228F\u2290\u2291\u2292\u2293\u2294\u2295\u2296\u2297\u2298\u2299\u229A\u229B\u229C\u229D\u229E\u229F\u22A0\u22A1\u22A2\u22A3\u22A4\u22A5\u22A6\u22A7\u22A8\u22A9\u22AA\u22AB\u22AC\u22AD\u22AE\u22AF\u22B0\u22B1\u22B2\u22B3\u22B4\u22B5\u22B6\u22B7\u22B8\u22B9\u22BA\u22BB\u22BC\u22BD\u22BE\u22BF\u22C0\u22C1\u22C2\u22C3\u22C4\u22C5\u22C6\u22C7\u22C8\u22C9\u22CA\u22CB\u22CC\u22CD\u22CE\u22CF\u22D0\u22D1\u22D2\u22D3\u22D4\u22D5\u22D6\u22D7\u22D8\u22D9\u22DA\u22DB\u22DC\u22DD\u22DE\u22DF\u22E0\u22E1\u22E2\u22E3\u22E4\u22E5\u22E6\u22E7\u22E8\u22E9\u22EA\u22EB\u22EC\u22ED\u22EE\u22EF\u22F0\u22F1\u22F2\u22F3\u22F4\u22F5\u22F6\u22F7\u22F8\u22F9\u22FA\u22FB\u22FC\u22FD\u22FE\u22FF\u2300\u2301\u2302\u2303\u2304\u2305\u2306\u2307\u2308\u2309\u230A\u230B\u230C\u230D\u230E\u230F\u2310\u2311\u2312\u2313\u2314\u2315\u2316\u2317\u2318\u2319\u231A\u231B\u231C\u231D\u231E\u231F\u2320\u2321\u2322\u2323\u2324\u2325\u2326\u2327\u2328\u2329\u232A\u232B\u232C\u232D\u232E\u232F\u2330\u2331\u2332\u2333\u2334\u2335\u2336\u2337\u2338\u2339\u233A\u233B\u233C\u233D\u233E\u233F\u2340\u2341\u2342\u2343\u2344\u2345\u2346\u2347\u2348\u2349\u234A\u234B\u234C\u234D\u234E\u234F\u2350\u2351\u2352\u2353\u2354\u2355\u2356\u2357\u2358\u2359\u235A\u235B\u235C\u235D\u235E\u235F\u2360\u2361\u2362\u2363\u2364\u2365\u2366\u2367\u2368\u2369\u236A\u236B\u236C\u236D\u236E\u236F\u2370\u2371\u2372\u2373\u2374\u2375\u2376\u2377\u2378\u2379\u237A\u237B\u237C\u237D\u237E\u237F\u2380\u2381\u2382\u2383\u2384\u2385\u2386\u2387\u2388\u2389\u238A\u238B\u238C\u238D\u238E\u238F\u2390\u2391\u2392\u2393\u2394\u2395\u2396\u2397\u2398\u2399\u239A\u239B\u239C\u239D\u239E\u239F\u23A0\u23A1\u23A2\u23A3\u23A4\u23A5\u23A6\u23A7\u23A8\u23A9\u23AA\u23AB\u23AC\u23AD\u23AE\u23AF\u23B0\u23B1\u23B2\u23B3\u23B4\u23B5\u23B6\u23B7\u23B8\u23B9\u23BA\u23BB\u23BC\u23BD\u23BE\u23BF\u23C0\u23C1\u23C2\u23C3\u23C4\u23C5\u23C6\u23C7\u23C8\u23C9\u23CA\u23CB\u23CC\u23CD\u23CE\u23CF\u23D0\u23D1\u23D2\u23D3\u23D4\u23D5\u23D6\u23D7\u23D8\u23D9\u23DA\u23DB\u23DC\u23DD\u23DE\u23DF\u23E0\u23E1\u23E2\u23E3\u23E4\u23E5\u23E6\u23E7\u23E8\u23E9\u23EA\u23EB\u23EC\u23ED\u23EE\u23EF\u23F0\u23F1\u23F2\u23F3\u23F4\u23F5\u23F6\u23F7\u23F8\u23F9\u23FA\u23FB\u23FC\u23FD\u23FE\u23FF"; +if (i8.replace(/\s+/g, "") !== o8) { + $ERROR("#8: Error matching character class \s between character 2000 and 23ff"); +} + +var i9 = ""; +for (var j = 9216; j < 10240; j++) + i9 += String.fromCharCode(j); +var o9 = i9; +if (i9.replace(/\s+/g, "") !== o9) { + $ERROR("#9: Error matching character class \s between character 2400 and 27ff"); +} + +var i10 = ""; +for (var j = 10240; j < 11264; j++) + i10 += String.fromCharCode(j); +var o10 = i10; +if (i10.replace(/\s+/g, "") !== o10) { + $ERROR("#10: Error matching character class \s between character 2800 and 2bff"); +} + +var i11 = ""; +for (var j = 11264; j < 12288; j++) + i11 += String.fromCharCode(j); +var o11 = i11; +if (i11.replace(/\s+/g, "") !== o11) { + $ERROR("#11: Error matching character class \s between character 2c00 and 2fff"); +} + +var i12 = ""; +for (var j = 12288; j < 13312; j++) + i12 += String.fromCharCode(j); +var o12 = "\u3001\u3002\u3003\u3004\u3005\u3006\u3007\u3008\u3009\u300A\u300B\u300C\u300D\u300E\u300F\u3010\u3011\u3012\u3013\u3014\u3015\u3016\u3017\u3018\u3019\u301A\u301B\u301C\u301D\u301E\u301F\u3020\u3021\u3022\u3023\u3024\u3025\u3026\u3027\u3028\u3029\u302A\u302B\u302C\u302D\u302E\u302F\u3030\u3031\u3032\u3033\u3034\u3035\u3036\u3037\u3038\u3039\u303A\u303B\u303C\u303D\u303E\u303F\u3040\u3041\u3042\u3043\u3044\u3045\u3046\u3047\u3048\u3049\u304A\u304B\u304C\u304D\u304E\u304F\u3050\u3051\u3052\u3053\u3054\u3055\u3056\u3057\u3058\u3059\u305A\u305B\u305C\u305D\u305E\u305F\u3060\u3061\u3062\u3063\u3064\u3065\u3066\u3067\u3068\u3069\u306A\u306B\u306C\u306D\u306E\u306F\u3070\u3071\u3072\u3073\u3074\u3075\u3076\u3077\u3078\u3079\u307A\u307B\u307C\u307D\u307E\u307F\u3080\u3081\u3082\u3083\u3084\u3085\u3086\u3087\u3088\u3089\u308A\u308B\u308C\u308D\u308E\u308F\u3090\u3091\u3092\u3093\u3094\u3095\u3096\u3097\u3098\u3099\u309A\u309B\u309C\u309D\u309E\u309F\u30A0\u30A1\u30A2\u30A3\u30A4\u30A5\u30A6\u30A7\u30A8\u30A9\u30AA\u30AB\u30AC\u30AD\u30AE\u30AF\u30B0\u30B1\u30B2\u30B3\u30B4\u30B5\u30B6\u30B7\u30B8\u30B9\u30BA\u30BB\u30BC\u30BD\u30BE\u30BF\u30C0\u30C1\u30C2\u30C3\u30C4\u30C5\u30C6\u30C7\u30C8\u30C9\u30CA\u30CB\u30CC\u30CD\u30CE\u30CF\u30D0\u30D1\u30D2\u30D3\u30D4\u30D5\u30D6\u30D7\u30D8\u30D9\u30DA\u30DB\u30DC\u30DD\u30DE\u30DF\u30E0\u30E1\u30E2\u30E3\u30E4\u30E5\u30E6\u30E7\u30E8\u30E9\u30EA\u30EB\u30EC\u30ED\u30EE\u30EF\u30F0\u30F1\u30F2\u30F3\u30F4\u30F5\u30F6\u30F7\u30F8\u30F9\u30FA\u30FB\u30FC\u30FD\u30FE\u30FF\u3100\u3101\u3102\u3103\u3104\u3105\u3106\u3107\u3108\u3109\u310A\u310B\u310C\u310D\u310E\u310F\u3110\u3111\u3112\u3113\u3114\u3115\u3116\u3117\u3118\u3119\u311A\u311B\u311C\u311D\u311E\u311F\u3120\u3121\u3122\u3123\u3124\u3125\u3126\u3127\u3128\u3129\u312A\u312B\u312C\u312D\u312E\u312F\u3130\u3131\u3132\u3133\u3134\u3135\u3136\u3137\u3138\u3139\u313A\u313B\u313C\u313D\u313E\u313F\u3140\u3141\u3142\u3143\u3144\u3145\u3146\u3147\u3148\u3149\u314A\u314B\u314C\u314D\u314E\u314F\u3150\u3151\u3152\u3153\u3154\u3155\u3156\u3157\u3158\u3159\u315A\u315B\u315C\u315D\u315E\u315F\u3160\u3161\u3162\u3163\u3164\u3165\u3166\u3167\u3168\u3169\u316A\u316B\u316C\u316D\u316E\u316F\u3170\u3171\u3172\u3173\u3174\u3175\u3176\u3177\u3178\u3179\u317A\u317B\u317C\u317D\u317E\u317F\u3180\u3181\u3182\u3183\u3184\u3185\u3186\u3187\u3188\u3189\u318A\u318B\u318C\u318D\u318E\u318F\u3190\u3191\u3192\u3193\u3194\u3195\u3196\u3197\u3198\u3199\u319A\u319B\u319C\u319D\u319E\u319F\u31A0\u31A1\u31A2\u31A3\u31A4\u31A5\u31A6\u31A7\u31A8\u31A9\u31AA\u31AB\u31AC\u31AD\u31AE\u31AF\u31B0\u31B1\u31B2\u31B3\u31B4\u31B5\u31B6\u31B7\u31B8\u31B9\u31BA\u31BB\u31BC\u31BD\u31BE\u31BF\u31C0\u31C1\u31C2\u31C3\u31C4\u31C5\u31C6\u31C7\u31C8\u31C9\u31CA\u31CB\u31CC\u31CD\u31CE\u31CF\u31D0\u31D1\u31D2\u31D3\u31D4\u31D5\u31D6\u31D7\u31D8\u31D9\u31DA\u31DB\u31DC\u31DD\u31DE\u31DF\u31E0\u31E1\u31E2\u31E3\u31E4\u31E5\u31E6\u31E7\u31E8\u31E9\u31EA\u31EB\u31EC\u31ED\u31EE\u31EF\u31F0\u31F1\u31F2\u31F3\u31F4\u31F5\u31F6\u31F7\u31F8\u31F9\u31FA\u31FB\u31FC\u31FD\u31FE\u31FF\u3200\u3201\u3202\u3203\u3204\u3205\u3206\u3207\u3208\u3209\u320A\u320B\u320C\u320D\u320E\u320F\u3210\u3211\u3212\u3213\u3214\u3215\u3216\u3217\u3218\u3219\u321A\u321B\u321C\u321D\u321E\u321F\u3220\u3221\u3222\u3223\u3224\u3225\u3226\u3227\u3228\u3229\u322A\u322B\u322C\u322D\u322E\u322F\u3230\u3231\u3232\u3233\u3234\u3235\u3236\u3237\u3238\u3239\u323A\u323B\u323C\u323D\u323E\u323F\u3240\u3241\u3242\u3243\u3244\u3245\u3246\u3247\u3248\u3249\u324A\u324B\u324C\u324D\u324E\u324F\u3250\u3251\u3252\u3253\u3254\u3255\u3256\u3257\u3258\u3259\u325A\u325B\u325C\u325D\u325E\u325F\u3260\u3261\u3262\u3263\u3264\u3265\u3266\u3267\u3268\u3269\u326A\u326B\u326C\u326D\u326E\u326F\u3270\u3271\u3272\u3273\u3274\u3275\u3276\u3277\u3278\u3279\u327A\u327B\u327C\u327D\u327E\u327F\u3280\u3281\u3282\u3283\u3284\u3285\u3286\u3287\u3288\u3289\u328A\u328B\u328C\u328D\u328E\u328F\u3290\u3291\u3292\u3293\u3294\u3295\u3296\u3297\u3298\u3299\u329A\u329B\u329C\u329D\u329E\u329F\u32A0\u32A1\u32A2\u32A3\u32A4\u32A5\u32A6\u32A7\u32A8\u32A9\u32AA\u32AB\u32AC\u32AD\u32AE\u32AF\u32B0\u32B1\u32B2\u32B3\u32B4\u32B5\u32B6\u32B7\u32B8\u32B9\u32BA\u32BB\u32BC\u32BD\u32BE\u32BF\u32C0\u32C1\u32C2\u32C3\u32C4\u32C5\u32C6\u32C7\u32C8\u32C9\u32CA\u32CB\u32CC\u32CD\u32CE\u32CF\u32D0\u32D1\u32D2\u32D3\u32D4\u32D5\u32D6\u32D7\u32D8\u32D9\u32DA\u32DB\u32DC\u32DD\u32DE\u32DF\u32E0\u32E1\u32E2\u32E3\u32E4\u32E5\u32E6\u32E7\u32E8\u32E9\u32EA\u32EB\u32EC\u32ED\u32EE\u32EF\u32F0\u32F1\u32F2\u32F3\u32F4\u32F5\u32F6\u32F7\u32F8\u32F9\u32FA\u32FB\u32FC\u32FD\u32FE\u32FF\u3300\u3301\u3302\u3303\u3304\u3305\u3306\u3307\u3308\u3309\u330A\u330B\u330C\u330D\u330E\u330F\u3310\u3311\u3312\u3313\u3314\u3315\u3316\u3317\u3318\u3319\u331A\u331B\u331C\u331D\u331E\u331F\u3320\u3321\u3322\u3323\u3324\u3325\u3326\u3327\u3328\u3329\u332A\u332B\u332C\u332D\u332E\u332F\u3330\u3331\u3332\u3333\u3334\u3335\u3336\u3337\u3338\u3339\u333A\u333B\u333C\u333D\u333E\u333F\u3340\u3341\u3342\u3343\u3344\u3345\u3346\u3347\u3348\u3349\u334A\u334B\u334C\u334D\u334E\u334F\u3350\u3351\u3352\u3353\u3354\u3355\u3356\u3357\u3358\u3359\u335A\u335B\u335C\u335D\u335E\u335F\u3360\u3361\u3362\u3363\u3364\u3365\u3366\u3367\u3368\u3369\u336A\u336B\u336C\u336D\u336E\u336F\u3370\u3371\u3372\u3373\u3374\u3375\u3376\u3377\u3378\u3379\u337A\u337B\u337C\u337D\u337E\u337F\u3380\u3381\u3382\u3383\u3384\u3385\u3386\u3387\u3388\u3389\u338A\u338B\u338C\u338D\u338E\u338F\u3390\u3391\u3392\u3393\u3394\u3395\u3396\u3397\u3398\u3399\u339A\u339B\u339C\u339D\u339E\u339F\u33A0\u33A1\u33A2\u33A3\u33A4\u33A5\u33A6\u33A7\u33A8\u33A9\u33AA\u33AB\u33AC\u33AD\u33AE\u33AF\u33B0\u33B1\u33B2\u33B3\u33B4\u33B5\u33B6\u33B7\u33B8\u33B9\u33BA\u33BB\u33BC\u33BD\u33BE\u33BF\u33C0\u33C1\u33C2\u33C3\u33C4\u33C5\u33C6\u33C7\u33C8\u33C9\u33CA\u33CB\u33CC\u33CD\u33CE\u33CF\u33D0\u33D1\u33D2\u33D3\u33D4\u33D5\u33D6\u33D7\u33D8\u33D9\u33DA\u33DB\u33DC\u33DD\u33DE\u33DF\u33E0\u33E1\u33E2\u33E3\u33E4\u33E5\u33E6\u33E7\u33E8\u33E9\u33EA\u33EB\u33EC\u33ED\u33EE\u33EF\u33F0\u33F1\u33F2\u33F3\u33F4\u33F5\u33F6\u33F7\u33F8\u33F9\u33FA\u33FB\u33FC\u33FD\u33FE\u33FF"; +if (i12.replace(/\s+/g, "") !== o12) { + $ERROR("#12: Error matching character class \s between character 3000 and 33ff"); +} + +var i13 = ""; +for (var j = 13312; j < 14336; j++) + i13 += String.fromCharCode(j); +var o13 = i13; +if (i13.replace(/\s+/g, "") !== o13) { + $ERROR("#13: Error matching character class \s between character 3400 and 37ff"); +} + +var i14 = ""; +for (var j = 14336; j < 15360; j++) + i14 += String.fromCharCode(j); +var o14 = i14; +if (i14.replace(/\s+/g, "") !== o14) { + $ERROR("#14: Error matching character class \s between character 3800 and 3bff"); +} + +var i15 = ""; +for (var j = 15360; j < 16384; j++) + i15 += String.fromCharCode(j); +var o15 = i15; +if (i15.replace(/\s+/g, "") !== o15) { + $ERROR("#15: Error matching character class \s between character 3c00 and 3fff"); +} + +var i16 = ""; +for (var j = 16384; j < 17408; j++) + i16 += String.fromCharCode(j); +var o16 = i16; +if (i16.replace(/\s+/g, "") !== o16) { + $ERROR("#16: Error matching character class \s between character 4000 and 43ff"); +} + +var i17 = ""; +for (var j = 17408; j < 18432; j++) + i17 += String.fromCharCode(j); +var o17 = i17; +if (i17.replace(/\s+/g, "") !== o17) { + $ERROR("#17: Error matching character class \s between character 4400 and 47ff"); +} + +var i18 = ""; +for (var j = 18432; j < 19456; j++) + i18 += String.fromCharCode(j); +var o18 = i18; +if (i18.replace(/\s+/g, "") !== o18) { + $ERROR("#18: Error matching character class \s between character 4800 and 4bff"); +} + +var i19 = ""; +for (var j = 19456; j < 20480; j++) + i19 += String.fromCharCode(j); +var o19 = i19; +if (i19.replace(/\s+/g, "") !== o19) { + $ERROR("#19: Error matching character class \s between character 4c00 and 4fff"); +} + +var i20 = ""; +for (var j = 20480; j < 21504; j++) + i20 += String.fromCharCode(j); +var o20 = i20; +if (i20.replace(/\s+/g, "") !== o20) { + $ERROR("#20: Error matching character class \s between character 5000 and 53ff"); +} + +var i21 = ""; +for (var j = 21504; j < 22528; j++) + i21 += String.fromCharCode(j); +var o21 = i21; +if (i21.replace(/\s+/g, "") !== o21) { + $ERROR("#21: Error matching character class \s between character 5400 and 57ff"); +} + +var i22 = ""; +for (var j = 22528; j < 23552; j++) + i22 += String.fromCharCode(j); +var o22 = i22; +if (i22.replace(/\s+/g, "") !== o22) { + $ERROR("#22: Error matching character class \s between character 5800 and 5bff"); +} + +var i23 = ""; +for (var j = 23552; j < 24576; j++) + i23 += String.fromCharCode(j); +var o23 = i23; +if (i23.replace(/\s+/g, "") !== o23) { + $ERROR("#23: Error matching character class \s between character 5c00 and 5fff"); +} + +var i24 = ""; +for (var j = 24576; j < 25600; j++) + i24 += String.fromCharCode(j); +var o24 = i24; +if (i24.replace(/\s+/g, "") !== o24) { + $ERROR("#24: Error matching character class \s between character 6000 and 63ff"); +} + +var i25 = ""; +for (var j = 25600; j < 26624; j++) + i25 += String.fromCharCode(j); +var o25 = i25; +if (i25.replace(/\s+/g, "") !== o25) { + $ERROR("#25: Error matching character class \s between character 6400 and 67ff"); +} + +var i26 = ""; +for (var j = 26624; j < 27648; j++) + i26 += String.fromCharCode(j); +var o26 = i26; +if (i26.replace(/\s+/g, "") !== o26) { + $ERROR("#26: Error matching character class \s between character 6800 and 6bff"); +} + +var i27 = ""; +for (var j = 27648; j < 28672; j++) + i27 += String.fromCharCode(j); +var o27 = i27; +if (i27.replace(/\s+/g, "") !== o27) { + $ERROR("#27: Error matching character class \s between character 6c00 and 6fff"); +} + +var i28 = ""; +for (var j = 28672; j < 29696; j++) + i28 += String.fromCharCode(j); +var o28 = i28; +if (i28.replace(/\s+/g, "") !== o28) { + $ERROR("#28: Error matching character class \s between character 7000 and 73ff"); +} + +var i29 = ""; +for (var j = 29696; j < 30720; j++) + i29 += String.fromCharCode(j); +var o29 = i29; +if (i29.replace(/\s+/g, "") !== o29) { + $ERROR("#29: Error matching character class \s between character 7400 and 77ff"); +} + +var i30 = ""; +for (var j = 30720; j < 31744; j++) + i30 += String.fromCharCode(j); +var o30 = i30; +if (i30.replace(/\s+/g, "") !== o30) { + $ERROR("#30: Error matching character class \s between character 7800 and 7bff"); +} + +var i31 = ""; +for (var j = 31744; j < 32768; j++) + i31 += String.fromCharCode(j); +var o31 = i31; +if (i31.replace(/\s+/g, "") !== o31) { + $ERROR("#31: Error matching character class \s between character 7c00 and 7fff"); +} + +var i32 = ""; +for (var j = 32768; j < 33792; j++) + i32 += String.fromCharCode(j); +var o32 = i32; +if (i32.replace(/\s+/g, "") !== o32) { + $ERROR("#32: Error matching character class \s between character 8000 and 83ff"); +} + +var i33 = ""; +for (var j = 33792; j < 34816; j++) + i33 += String.fromCharCode(j); +var o33 = i33; +if (i33.replace(/\s+/g, "") !== o33) { + $ERROR("#33: Error matching character class \s between character 8400 and 87ff"); +} + +var i34 = ""; +for (var j = 34816; j < 35840; j++) + i34 += String.fromCharCode(j); +var o34 = i34; +if (i34.replace(/\s+/g, "") !== o34) { + $ERROR("#34: Error matching character class \s between character 8800 and 8bff"); +} + +var i35 = ""; +for (var j = 35840; j < 36864; j++) + i35 += String.fromCharCode(j); +var o35 = i35; +if (i35.replace(/\s+/g, "") !== o35) { + $ERROR("#35: Error matching character class \s between character 8c00 and 8fff"); +} + +var i36 = ""; +for (var j = 36864; j < 37888; j++) + i36 += String.fromCharCode(j); +var o36 = i36; +if (i36.replace(/\s+/g, "") !== o36) { + $ERROR("#36: Error matching character class \s between character 9000 and 93ff"); +} + +var i37 = ""; +for (var j = 37888; j < 38912; j++) + i37 += String.fromCharCode(j); +var o37 = i37; +if (i37.replace(/\s+/g, "") !== o37) { + $ERROR("#37: Error matching character class \s between character 9400 and 97ff"); +} + +var i38 = ""; +for (var j = 38912; j < 39936; j++) + i38 += String.fromCharCode(j); +var o38 = i38; +if (i38.replace(/\s+/g, "") !== o38) { + $ERROR("#38: Error matching character class \s between character 9800 and 9bff"); +} + +var i39 = ""; +for (var j = 39936; j < 40960; j++) + i39 += String.fromCharCode(j); +var o39 = i39; +if (i39.replace(/\s+/g, "") !== o39) { + $ERROR("#39: Error matching character class \s between character 9c00 and 9fff"); +} + +var i40 = ""; +for (var j = 40960; j < 41984; j++) + i40 += String.fromCharCode(j); +var o40 = i40; +if (i40.replace(/\s+/g, "") !== o40) { + $ERROR("#40: Error matching character class \s between character a000 and a3ff"); +} + +var i41 = ""; +for (var j = 41984; j < 43008; j++) + i41 += String.fromCharCode(j); +var o41 = i41; +if (i41.replace(/\s+/g, "") !== o41) { + $ERROR("#41: Error matching character class \s between character a400 and a7ff"); +} + +var i42 = ""; +for (var j = 43008; j < 44032; j++) + i42 += String.fromCharCode(j); +var o42 = i42; +if (i42.replace(/\s+/g, "") !== o42) { + $ERROR("#42: Error matching character class \s between character a800 and abff"); +} + +var i43 = ""; +for (var j = 44032; j < 45056; j++) + i43 += String.fromCharCode(j); +var o43 = i43; +if (i43.replace(/\s+/g, "") !== o43) { + $ERROR("#43: Error matching character class \s between character ac00 and afff"); +} + +var i44 = ""; +for (var j = 45056; j < 46080; j++) + i44 += String.fromCharCode(j); +var o44 = i44; +if (i44.replace(/\s+/g, "") !== o44) { + $ERROR("#44: Error matching character class \s between character b000 and b3ff"); +} + +var i45 = ""; +for (var j = 46080; j < 47104; j++) + i45 += String.fromCharCode(j); +var o45 = i45; +if (i45.replace(/\s+/g, "") !== o45) { + $ERROR("#45: Error matching character class \s between character b400 and b7ff"); +} + +var i46 = ""; +for (var j = 47104; j < 48128; j++) + i46 += String.fromCharCode(j); +var o46 = i46; +if (i46.replace(/\s+/g, "") !== o46) { + $ERROR("#46: Error matching character class \s between character b800 and bbff"); +} + +var i47 = ""; +for (var j = 48128; j < 49152; j++) + i47 += String.fromCharCode(j); +var o47 = i47; +if (i47.replace(/\s+/g, "") !== o47) { + $ERROR("#47: Error matching character class \s between character bc00 and bfff"); +} + +var i48 = ""; +for (var j = 49152; j < 50176; j++) + i48 += String.fromCharCode(j); +var o48 = i48; +if (i48.replace(/\s+/g, "") !== o48) { + $ERROR("#48: Error matching character class \s between character c000 and c3ff"); +} + +var i49 = ""; +for (var j = 50176; j < 51200; j++) + i49 += String.fromCharCode(j); +var o49 = i49; +if (i49.replace(/\s+/g, "") !== o49) { + $ERROR("#49: Error matching character class \s between character c400 and c7ff"); +} + +var i50 = ""; +for (var j = 51200; j < 52224; j++) + i50 += String.fromCharCode(j); +var o50 = i50; +if (i50.replace(/\s+/g, "") !== o50) { + $ERROR("#50: Error matching character class \s between character c800 and cbff"); +} + +var i51 = ""; +for (var j = 52224; j < 53248; j++) + i51 += String.fromCharCode(j); +var o51 = i51; +if (i51.replace(/\s+/g, "") !== o51) { + $ERROR("#51: Error matching character class \s between character cc00 and cfff"); +} + +var i52 = ""; +for (var j = 53248; j < 54272; j++) + i52 += String.fromCharCode(j); +var o52 = i52; +if (i52.replace(/\s+/g, "") !== o52) { + $ERROR("#52: Error matching character class \s between character d000 and d3ff"); +} + +var i53 = ""; +for (var j = 54272; j < 55296; j++) + i53 += String.fromCharCode(j); +var o53 = i53; +if (i53.replace(/\s+/g, "") !== o53) { + $ERROR("#53: Error matching character class \s between character d400 and d7ff"); +} + +var i54 = ""; +for (var j = 55296; j < 56320; j++) + i54 += String.fromCharCode(j); +var o54 = i54; +if (i54.replace(/\s+/g, "") !== o54) { + $ERROR("#54: Error matching character class \s between character d800 and dbff"); +} + +var i55 = ""; +for (var j = 56320; j < 57344; j++) + i55 += String.fromCharCode(j); +var o55 = i55; +if (i55.replace(/\s+/g, "") !== o55) { + $ERROR("#55: Error matching character class \s between character dc00 and dfff"); +} + +var i56 = ""; +for (var j = 57344; j < 58368; j++) + i56 += String.fromCharCode(j); +var o56 = i56; +if (i56.replace(/\s+/g, "") !== o56) { + $ERROR("#56: Error matching character class \s between character e000 and e3ff"); +} + +var i57 = ""; +for (var j = 58368; j < 59392; j++) + i57 += String.fromCharCode(j); +var o57 = i57; +if (i57.replace(/\s+/g, "") !== o57) { + $ERROR("#57: Error matching character class \s between character e400 and e7ff"); +} + +var i58 = ""; +for (var j = 59392; j < 60416; j++) + i58 += String.fromCharCode(j); +var o58 = i58; +if (i58.replace(/\s+/g, "") !== o58) { + $ERROR("#58: Error matching character class \s between character e800 and ebff"); +} + +var i59 = ""; +for (var j = 60416; j < 61440; j++) + i59 += String.fromCharCode(j); +var o59 = i59; +if (i59.replace(/\s+/g, "") !== o59) { + $ERROR("#59: Error matching character class \s between character ec00 and efff"); +} + +var i60 = ""; +for (var j = 61440; j < 62464; j++) + i60 += String.fromCharCode(j); +var o60 = i60; +if (i60.replace(/\s+/g, "") !== o60) { + $ERROR("#60: Error matching character class \s between character f000 and f3ff"); +} + +var i61 = ""; +for (var j = 62464; j < 63488; j++) + i61 += String.fromCharCode(j); +var o61 = i61; +if (i61.replace(/\s+/g, "") !== o61) { + $ERROR("#61: Error matching character class \s between character f400 and f7ff"); +} + +var i62 = ""; +for (var j = 63488; j < 64512; j++) + i62 += String.fromCharCode(j); +var o62 = i62; +if (i62.replace(/\s+/g, "") !== o62) { + $ERROR("#62: Error matching character class \s between character f800 and fbff"); +} + +var i63 = ""; +for (var j = 64512; j < 65536; j++) { + if (j === 0xFEFF) { continue; } //Ignore BOM + i63 += String.fromCharCode(j); +} +var o63 = i63; +if (i63.replace(/\s+/g, "") !== o63) { + $ERROR("#63: Error matching character class \s between character fc00 and ffff"); +} + +var i64 = String.fromCharCode(0xFEFF); +if (i64.replace(/\s/g, "") !== "") { + $ERROR("#64: Error matching character class \s for BOM (feff)"); +} diff --git a/goja/token/Makefile b/goja/token/Makefile new file mode 100644 index 0000000..1e85c73 --- /dev/null +++ b/goja/token/Makefile @@ -0,0 +1,2 @@ +token_const.go: tokenfmt + ./$^ | gofmt > $@ diff --git a/goja/token/README.markdown b/goja/token/README.markdown new file mode 100644 index 0000000..66dd2ab --- /dev/null +++ b/goja/token/README.markdown @@ -0,0 +1,171 @@ +# token +-- + import "github.com/dop251/goja/token" + +Package token defines constants representing the lexical tokens of JavaScript +(ECMA5). + +## Usage + +```go +const ( + ILLEGAL + EOF + COMMENT + KEYWORD + + STRING + BOOLEAN + NULL + NUMBER + IDENTIFIER + + PLUS // + + MINUS // - + MULTIPLY // * + SLASH // / + REMAINDER // % + + AND // & + OR // | + EXCLUSIVE_OR // ^ + SHIFT_LEFT // << + SHIFT_RIGHT // >> + UNSIGNED_SHIFT_RIGHT // >>> + AND_NOT // &^ + + ADD_ASSIGN // += + SUBTRACT_ASSIGN // -= + MULTIPLY_ASSIGN // *= + QUOTIENT_ASSIGN // /= + REMAINDER_ASSIGN // %= + + AND_ASSIGN // &= + OR_ASSIGN // |= + EXCLUSIVE_OR_ASSIGN // ^= + SHIFT_LEFT_ASSIGN // <<= + SHIFT_RIGHT_ASSIGN // >>= + UNSIGNED_SHIFT_RIGHT_ASSIGN // >>>= + AND_NOT_ASSIGN // &^= + + LOGICAL_AND // && + LOGICAL_OR // || + INCREMENT // ++ + DECREMENT // -- + + EQUAL // == + STRICT_EQUAL // === + LESS // < + GREATER // > + ASSIGN // = + NOT // ! + + BITWISE_NOT // ~ + + NOT_EQUAL // != + STRICT_NOT_EQUAL // !== + LESS_OR_EQUAL // <= + GREATER_OR_EQUAL // >= + + LEFT_PARENTHESIS // ( + LEFT_BRACKET // [ + LEFT_BRACE // { + COMMA // , + PERIOD // . + + RIGHT_PARENTHESIS // ) + RIGHT_BRACKET // ] + RIGHT_BRACE // } + SEMICOLON // ; + COLON // : + QUESTION_MARK // ? + + IF + IN + DO + + VAR + FOR + NEW + TRY + + THIS + ELSE + CASE + VOID + WITH + + WHILE + BREAK + CATCH + THROW + + RETURN + TYPEOF + DELETE + SWITCH + + DEFAULT + FINALLY + + FUNCTION + CONTINUE + DEBUGGER + + INSTANCEOF +) +``` + +#### type Token + +```go +type Token int +``` + +Token is the set of lexical tokens in JavaScript (ECMA5). + +#### func IsKeyword + +```go +func IsKeyword(literal string) (Token, bool) +``` +IsKeyword returns the keyword token if literal is a keyword, a KEYWORD token if +the literal is a future keyword (const, let, class, super, ...), or 0 if the +literal is not a keyword. + +If the literal is a keyword, IsKeyword returns a second value indicating if the +literal is considered a future keyword in strict-mode only. + +7.6.1.2 Future Reserved Words: + + const + class + enum + export + extends + import + super + +7.6.1.2 Future Reserved Words (strict): + + implements + interface + let + package + private + protected + public + static + +#### func (Token) String + +```go +func (tkn Token) String() string +``` +String returns the string corresponding to the token. For operators, delimiters, +and keywords the string is the actual token string (e.g., for the token PLUS, +the String() is "+"). For all other tokens the string corresponds to the token +name (e.g. for the token IDENTIFIER, the string is "IDENTIFIER"). + +-- +**godocdown** http://github.com/robertkrimen/godocdown diff --git a/goja/token/token.go b/goja/token/token.go new file mode 100644 index 0000000..8527137 --- /dev/null +++ b/goja/token/token.go @@ -0,0 +1,122 @@ +// Package token defines constants representing the lexical tokens of JavaScript (ECMA5). +package token + +import ( + "strconv" +) + +// Token is the set of lexical tokens in JavaScript (ECMA5). +type Token int + +// String returns the string corresponding to the token. +// For operators, delimiters, and keywords the string is the actual +// token string (e.g., for the token PLUS, the String() is +// "+"). For all other tokens the string corresponds to the token +// name (e.g. for the token IDENTIFIER, the string is "IDENTIFIER"). +func (tkn Token) String() string { + if tkn == 0 { + return "UNKNOWN" + } + if tkn < Token(len(token2string)) { + return token2string[tkn] + } + return "token(" + strconv.Itoa(int(tkn)) + ")" +} + +//lint:ignore U1000 This is not used for anything +func (tkn Token) precedence(in bool) int { + + switch tkn { + case LOGICAL_OR: + return 1 + + case LOGICAL_AND: + return 2 + + case OR, OR_ASSIGN: + return 3 + + case EXCLUSIVE_OR: + return 4 + + case AND, AND_ASSIGN: + return 5 + + case EQUAL, + NOT_EQUAL, + STRICT_EQUAL, + STRICT_NOT_EQUAL: + return 6 + + case LESS, GREATER, LESS_OR_EQUAL, GREATER_OR_EQUAL, INSTANCEOF: + return 7 + + case IN: + if in { + return 7 + } + return 0 + + case SHIFT_LEFT, SHIFT_RIGHT, UNSIGNED_SHIFT_RIGHT: + fallthrough + case SHIFT_LEFT_ASSIGN, SHIFT_RIGHT_ASSIGN, UNSIGNED_SHIFT_RIGHT_ASSIGN: + return 8 + + case PLUS, MINUS, ADD_ASSIGN, SUBTRACT_ASSIGN: + return 9 + + case MULTIPLY, SLASH, REMAINDER, MULTIPLY_ASSIGN, QUOTIENT_ASSIGN, REMAINDER_ASSIGN: + return 11 + } + return 0 +} + +type _keyword struct { + token Token + futureKeyword bool + strict bool +} + +// IsKeyword returns the keyword token if literal is a keyword, a KEYWORD token +// if the literal is a future keyword (const, let, class, super, ...), or 0 if the literal is not a keyword. +// +// If the literal is a keyword, IsKeyword returns a second value indicating if the literal +// is considered a future keyword in strict-mode only. +// +// 7.6.1.2 Future Reserved Words: +// +// const +// class +// enum +// export +// extends +// import +// super +// +// 7.6.1.2 Future Reserved Words (strict): +// +// implements +// interface +// let +// package +// private +// protected +// public +// static +func IsKeyword(literal string) (Token, bool) { + if keyword, exists := keywordTable[literal]; exists { + if keyword.futureKeyword { + return KEYWORD, keyword.strict + } + return keyword.token, false + } + return 0, false +} + +func IsId(tkn Token) bool { + return tkn >= IDENTIFIER +} + +func IsUnreservedWord(tkn Token) bool { + return tkn > ESCAPED_RESERVED_WORD +} diff --git a/goja/token/token_const.go b/goja/token/token_const.go new file mode 100644 index 0000000..2591427 --- /dev/null +++ b/goja/token/token_const.go @@ -0,0 +1,400 @@ +package token + +const ( + _ Token = iota + + ILLEGAL + EOF + COMMENT + + STRING + NUMBER + + PLUS // + + MINUS // - + MULTIPLY // * + EXPONENT // ** + SLASH // / + REMAINDER // % + + AND // & + OR // | + EXCLUSIVE_OR // ^ + SHIFT_LEFT // << + SHIFT_RIGHT // >> + UNSIGNED_SHIFT_RIGHT // >>> + + ADD_ASSIGN // += + SUBTRACT_ASSIGN // -= + MULTIPLY_ASSIGN // *= + EXPONENT_ASSIGN // **= + QUOTIENT_ASSIGN // /= + REMAINDER_ASSIGN // %= + + AND_ASSIGN // &= + OR_ASSIGN // |= + EXCLUSIVE_OR_ASSIGN // ^= + SHIFT_LEFT_ASSIGN // <<= + SHIFT_RIGHT_ASSIGN // >>= + UNSIGNED_SHIFT_RIGHT_ASSIGN // >>>= + + LOGICAL_AND // && + LOGICAL_OR // || + COALESCE // ?? + INCREMENT // ++ + DECREMENT // -- + + EQUAL // == + STRICT_EQUAL // === + LESS // < + GREATER // > + ASSIGN // = + NOT // ! + + BITWISE_NOT // ~ + + NOT_EQUAL // != + STRICT_NOT_EQUAL // !== + LESS_OR_EQUAL // <= + GREATER_OR_EQUAL // >= + + LEFT_PARENTHESIS // ( + LEFT_BRACKET // [ + LEFT_BRACE // { + COMMA // , + PERIOD // . + + RIGHT_PARENTHESIS // ) + RIGHT_BRACKET // ] + RIGHT_BRACE // } + SEMICOLON // ; + COLON // : + QUESTION_MARK // ? + QUESTION_DOT // ?. + ARROW // => + ELLIPSIS // ... + BACKTICK // ` + + PRIVATE_IDENTIFIER + + // tokens below (and only them) are syntactically valid identifiers + + IDENTIFIER + KEYWORD + BOOLEAN + NULL + + IF + IN + OF + DO + + VAR + FOR + NEW + TRY + + THIS + ELSE + CASE + VOID + WITH + + CONST + WHILE + BREAK + CATCH + THROW + CLASS + SUPER + + RETURN + TYPEOF + DELETE + SWITCH + + DEFAULT + FINALLY + EXTENDS + + FUNCTION + CONTINUE + DEBUGGER + + INSTANCEOF + + ESCAPED_RESERVED_WORD + // Non-reserved keywords below + + LET + STATIC + ASYNC + AWAIT + YIELD +) + +var token2string = [...]string{ + ILLEGAL: "ILLEGAL", + EOF: "EOF", + COMMENT: "COMMENT", + KEYWORD: "KEYWORD", + STRING: "STRING", + BOOLEAN: "BOOLEAN", + NULL: "NULL", + NUMBER: "NUMBER", + IDENTIFIER: "IDENTIFIER", + PRIVATE_IDENTIFIER: "PRIVATE_IDENTIFIER", + PLUS: "+", + MINUS: "-", + EXPONENT: "**", + MULTIPLY: "*", + SLASH: "/", + REMAINDER: "%", + AND: "&", + OR: "|", + EXCLUSIVE_OR: "^", + SHIFT_LEFT: "<<", + SHIFT_RIGHT: ">>", + UNSIGNED_SHIFT_RIGHT: ">>>", + ADD_ASSIGN: "+=", + SUBTRACT_ASSIGN: "-=", + MULTIPLY_ASSIGN: "*=", + EXPONENT_ASSIGN: "**=", + QUOTIENT_ASSIGN: "/=", + REMAINDER_ASSIGN: "%=", + AND_ASSIGN: "&=", + OR_ASSIGN: "|=", + EXCLUSIVE_OR_ASSIGN: "^=", + SHIFT_LEFT_ASSIGN: "<<=", + SHIFT_RIGHT_ASSIGN: ">>=", + UNSIGNED_SHIFT_RIGHT_ASSIGN: ">>>=", + LOGICAL_AND: "&&", + LOGICAL_OR: "||", + COALESCE: "??", + INCREMENT: "++", + DECREMENT: "--", + EQUAL: "==", + STRICT_EQUAL: "===", + LESS: "<", + GREATER: ">", + ASSIGN: "=", + NOT: "!", + BITWISE_NOT: "~", + NOT_EQUAL: "!=", + STRICT_NOT_EQUAL: "!==", + LESS_OR_EQUAL: "<=", + GREATER_OR_EQUAL: ">=", + LEFT_PARENTHESIS: "(", + LEFT_BRACKET: "[", + LEFT_BRACE: "{", + COMMA: ",", + PERIOD: ".", + RIGHT_PARENTHESIS: ")", + RIGHT_BRACKET: "]", + RIGHT_BRACE: "}", + SEMICOLON: ";", + COLON: ":", + QUESTION_MARK: "?", + QUESTION_DOT: "?.", + ARROW: "=>", + ELLIPSIS: "...", + BACKTICK: "`", + IF: "if", + IN: "in", + OF: "of", + DO: "do", + VAR: "var", + LET: "let", + FOR: "for", + NEW: "new", + TRY: "try", + THIS: "this", + ELSE: "else", + CASE: "case", + VOID: "void", + WITH: "with", + ASYNC: "async", + AWAIT: "await", + YIELD: "yield", + CONST: "const", + WHILE: "while", + BREAK: "break", + CATCH: "catch", + THROW: "throw", + CLASS: "class", + SUPER: "super", + RETURN: "return", + TYPEOF: "typeof", + DELETE: "delete", + SWITCH: "switch", + STATIC: "static", + DEFAULT: "default", + FINALLY: "finally", + EXTENDS: "extends", + FUNCTION: "function", + CONTINUE: "continue", + DEBUGGER: "debugger", + INSTANCEOF: "instanceof", +} + +var keywordTable = map[string]_keyword{ + "if": { + token: IF, + }, + "in": { + token: IN, + }, + "do": { + token: DO, + }, + "var": { + token: VAR, + }, + "for": { + token: FOR, + }, + "new": { + token: NEW, + }, + "try": { + token: TRY, + }, + "this": { + token: THIS, + }, + "else": { + token: ELSE, + }, + "case": { + token: CASE, + }, + "void": { + token: VOID, + }, + "with": { + token: WITH, + }, + "async": { + token: ASYNC, + }, + "while": { + token: WHILE, + }, + "break": { + token: BREAK, + }, + "catch": { + token: CATCH, + }, + "throw": { + token: THROW, + }, + "return": { + token: RETURN, + }, + "typeof": { + token: TYPEOF, + }, + "delete": { + token: DELETE, + }, + "switch": { + token: SWITCH, + }, + "default": { + token: DEFAULT, + }, + "finally": { + token: FINALLY, + }, + "function": { + token: FUNCTION, + }, + "continue": { + token: CONTINUE, + }, + "debugger": { + token: DEBUGGER, + }, + "instanceof": { + token: INSTANCEOF, + }, + "const": { + token: CONST, + }, + "class": { + token: CLASS, + }, + "enum": { + token: KEYWORD, + futureKeyword: true, + }, + "export": { + token: KEYWORD, + futureKeyword: true, + }, + "extends": { + token: EXTENDS, + }, + "import": { + token: KEYWORD, + futureKeyword: true, + }, + "super": { + token: SUPER, + }, + /* + "implements": { + token: KEYWORD, + futureKeyword: true, + strict: true, + }, + "interface": { + token: KEYWORD, + futureKeyword: true, + strict: true, + },*/ + "let": { + token: LET, + strict: true, + }, + /*"package": { + token: KEYWORD, + futureKeyword: true, + strict: true, + }, + "private": { + token: KEYWORD, + futureKeyword: true, + strict: true, + }, + "protected": { + token: KEYWORD, + futureKeyword: true, + strict: true, + }, + "public": { + token: KEYWORD, + futureKeyword: true, + strict: true, + },*/ + "static": { + token: STATIC, + strict: true, + }, + "await": { + token: AWAIT, + }, + "yield": { + token: YIELD, + }, + "false": { + token: BOOLEAN, + }, + "true": { + token: BOOLEAN, + }, + "null": { + token: NULL, + }, +} diff --git a/goja/token/tokenfmt b/goja/token/tokenfmt new file mode 100755 index 0000000..63dd5d9 --- /dev/null +++ b/goja/token/tokenfmt @@ -0,0 +1,222 @@ +#!/usr/bin/env perl + +use strict; +use warnings; + +my (%token, @order, @keywords); + +{ + my $keywords; + my @const; + push @const, <<_END_; +package token + +const( + _ Token = iota +_END_ + + for (split m/\n/, <<_END_) { +ILLEGAL +EOF +COMMENT +KEYWORD + +STRING +BOOLEAN +NULL +NUMBER +IDENTIFIER + +PLUS + +MINUS - +MULTIPLY * +SLASH / +REMAINDER % + +AND & +OR | +EXCLUSIVE_OR ^ +SHIFT_LEFT << +SHIFT_RIGHT >> +UNSIGNED_SHIFT_RIGHT >>> +AND_NOT &^ + +ADD_ASSIGN += +SUBTRACT_ASSIGN -= +MULTIPLY_ASSIGN *= +QUOTIENT_ASSIGN /= +REMAINDER_ASSIGN %= + +AND_ASSIGN &= +OR_ASSIGN |= +EXCLUSIVE_OR_ASSIGN ^= +SHIFT_LEFT_ASSIGN <<= +SHIFT_RIGHT_ASSIGN >>= +UNSIGNED_SHIFT_RIGHT_ASSIGN >>>= +AND_NOT_ASSIGN &^= + +LOGICAL_AND && +LOGICAL_OR || +INCREMENT ++ +DECREMENT -- + +EQUAL == +STRICT_EQUAL === +LESS < +GREATER > +ASSIGN = +NOT ! + +BITWISE_NOT ~ + +NOT_EQUAL != +STRICT_NOT_EQUAL !== +LESS_OR_EQUAL <= +GREATER_OR_EQUAL <= + +LEFT_PARENTHESIS ( +LEFT_BRACKET [ +LEFT_BRACE { +COMMA , +PERIOD . + +RIGHT_PARENTHESIS ) +RIGHT_BRACKET ] +RIGHT_BRACE } +SEMICOLON ; +COLON : +QUESTION_MARK ? + +firstKeyword +IF +IN +DO + +VAR +FOR +NEW +TRY + +THIS +ELSE +CASE +VOID +WITH + +WHILE +BREAK +CATCH +THROW + +RETURN +TYPEOF +DELETE +SWITCH + +DEFAULT +FINALLY + +FUNCTION +CONTINUE +DEBUGGER + +INSTANCEOF +lastKeyword +_END_ + chomp; + + next if m/^\s*#/; + + my ($name, $symbol) = m/(\w+)\s*(\S+)?/; + + if (defined $symbol) { + push @order, $name; + push @const, "$name // $symbol"; + $token{$name} = $symbol; + } elsif (defined $name) { + $keywords ||= $name eq 'firstKeyword'; + + push @const, $name; + #$const[-1] .= " Token = iota" if 2 == @const; + if ($name =~ m/^([A-Z]+)/) { + push @keywords, $name if $keywords; + push @order, $name; + if ($token{SEMICOLON}) { + $token{$name} = lc $1; + } else { + $token{$name} = $name; + } + } + } else { + push @const, ""; + } + + } + push @const, ")"; + print join "\n", @const, ""; +} + +{ + print <<_END_; + +var token2string = [...]string{ +_END_ + for my $name (@order) { + print "$name: \"$token{$name}\",\n"; + } + print <<_END_; +} +_END_ + + print <<_END_; + +var keywordTable = map[string]_keyword{ +_END_ + for my $name (@keywords) { + print <<_END_ + "@{[ lc $name ]}": _keyword{ + token: $name, + }, +_END_ + } + + for my $name (qw/ + const + class + enum + export + extends + import + super + /) { + print <<_END_ + "$name": _keyword{ + token: KEYWORD, + futureKeyword: true, + }, +_END_ + } + + for my $name (qw/ + implements + interface + let + package + private + protected + public + static + /) { + print <<_END_ + "$name": _keyword{ + token: KEYWORD, + futureKeyword: true, + strict: true, + }, +_END_ + } + + print <<_END_; +} +_END_ +} diff --git a/goja/typedarrays.go b/goja/typedarrays.go new file mode 100644 index 0000000..67c4677 --- /dev/null +++ b/goja/typedarrays.go @@ -0,0 +1,1327 @@ +package goja + +import ( + "math" + "math/big" + "reflect" + "strconv" + "unsafe" + + "github.com/dop251/goja/unistring" +) + +type byteOrder bool + +const ( + bigEndian byteOrder = false + littleEndian byteOrder = true +) + +var ( + nativeEndian byteOrder + + arrayBufferType = reflect.TypeOf(ArrayBuffer{}) +) + +type typedArrayObjectCtor func(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject + +type arrayBufferObject struct { + baseObject + detached bool + data []byte +} + +// ArrayBuffer is a Go wrapper around ECMAScript ArrayBuffer. Calling Runtime.ToValue() on it +// returns the underlying ArrayBuffer. Calling Export() on an ECMAScript ArrayBuffer returns a wrapper. +// Use Runtime.NewArrayBuffer([]byte) to create one. +type ArrayBuffer struct { + buf *arrayBufferObject +} + +type dataViewObject struct { + baseObject + viewedArrayBuf *arrayBufferObject + byteLen, byteOffset int +} + +type typedArray interface { + toRaw(Value) uint64 + get(idx int) Value + set(idx int, value Value) + getRaw(idx int) uint64 + setRaw(idx int, raw uint64) + less(i, j int) bool + swap(i, j int) + typeMatch(v Value) bool + export(offset int, length int) interface{} + exportType() reflect.Type +} + +type uint8Array []byte +type uint8ClampedArray []byte +type int8Array []byte +type uint16Array []byte +type int16Array []byte +type uint32Array []byte +type int32Array []byte +type float32Array []byte +type float64Array []byte +type bigInt64Array []byte +type bigUint64Array []byte + +type typedArrayObject struct { + baseObject + viewedArrayBuf *arrayBufferObject + defaultCtor *Object + length, offset int + elemSize int + typedArray typedArray +} + +func (a ArrayBuffer) toValue(r *Runtime) Value { + if a.buf == nil { + return _null + } + v := a.buf.val + if v.runtime != r { + panic(r.NewTypeError("Illegal runtime transition of an ArrayBuffer")) + } + return v +} + +// Bytes returns the underlying []byte for this ArrayBuffer. +// For detached ArrayBuffers returns nil. +func (a ArrayBuffer) Bytes() []byte { + return a.buf.data +} + +// Detach the ArrayBuffer. After this, the underlying []byte becomes unreferenced and any attempt +// to use this ArrayBuffer results in a TypeError. +// Returns false if it was already detached, true otherwise. +// Note, this method may only be called from the goroutine that 'owns' the Runtime, it may not +// be called concurrently. +func (a ArrayBuffer) Detach() bool { + if a.buf.detached { + return false + } + a.buf.detach() + return true +} + +// Detached returns true if the ArrayBuffer is detached. +func (a ArrayBuffer) Detached() bool { + return a.buf.detached +} + +// NewArrayBuffer creates a new instance of ArrayBuffer backed by the provided byte slice. +// +// Warning: be careful when using unaligned slices (sub-slices that do not start at word boundaries). If later a +// typed array of a multibyte type (uint16, uint32, etc.) is created from a buffer backed by an unaligned slice, +// using this typed array will result in unaligned access which may cause performance degradation or runtime panics +// on some architectures or configurations. +func (r *Runtime) NewArrayBuffer(data []byte) ArrayBuffer { + buf := r._newArrayBuffer(r.getArrayBufferPrototype(), nil) + buf.data = data + return ArrayBuffer{ + buf: buf, + } +} + +func (a *uint8Array) toRaw(v Value) uint64 { + return uint64(toUint8(v)) +} + +func (a *uint8Array) ptr(idx int) *uint8 { + p := unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(a)).Data) + return (*uint8)(unsafe.Pointer(uintptr(p) + uintptr(idx))) +} + +func (a *uint8Array) get(idx int) Value { + return intToValue(int64(*(a.ptr(idx)))) +} + +func (a *uint8Array) set(idx int, value Value) { + *(a.ptr(idx)) = toUint8(value) +} + +func (a *uint8Array) getRaw(idx int) uint64 { + return uint64(*(a.ptr(idx))) +} + +func (a *uint8Array) setRaw(idx int, raw uint64) { + *(a.ptr(idx)) = uint8(raw) +} + +func (a *uint8Array) less(i, j int) bool { + return *(a.ptr(i)) < *(a.ptr(j)) +} + +func (a *uint8Array) swap(i, j int) { + pi, pj := a.ptr(i), a.ptr(j) + *pi, *pj = *pj, *pi +} + +func (a *uint8Array) typeMatch(v Value) bool { + if i, ok := v.(valueInt); ok { + return i >= 0 && i <= 255 + } + return false +} + +func (a *uint8Array) export(offset int, length int) interface{} { + return ([]uint8)(*a)[offset : offset+length : offset+length] +} + +func (a *uint8Array) exportType() reflect.Type { + return typeBytes +} + +func (a *uint8ClampedArray) toRaw(v Value) uint64 { + return uint64(toUint8Clamp(v)) +} + +func (a *uint8ClampedArray) ptr(idx int) *uint8 { + p := unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(a)).Data) + return (*uint8)(unsafe.Pointer(uintptr(p) + uintptr(idx))) +} + +func (a *uint8ClampedArray) get(idx int) Value { + return intToValue(int64(*(a.ptr(idx)))) +} + +func (a *uint8ClampedArray) set(idx int, value Value) { + *(a.ptr(idx)) = toUint8Clamp(value) +} + +func (a *uint8ClampedArray) getRaw(idx int) uint64 { + return uint64(*(a.ptr(idx))) +} + +func (a *uint8ClampedArray) setRaw(idx int, raw uint64) { + *(a.ptr(idx)) = uint8(raw) +} + +func (a *uint8ClampedArray) less(i, j int) bool { + return *(a.ptr(i)) < *(a.ptr(j)) +} + +func (a *uint8ClampedArray) swap(i, j int) { + pi, pj := a.ptr(i), a.ptr(j) + *pi, *pj = *pj, *pi +} + +func (a *uint8ClampedArray) typeMatch(v Value) bool { + if i, ok := v.(valueInt); ok { + return i >= 0 && i <= 255 + } + return false +} + +func (a *uint8ClampedArray) export(offset int, length int) interface{} { + return ([]uint8)(*a)[offset : offset+length : offset+length] +} + +func (a *uint8ClampedArray) exportType() reflect.Type { + return typeBytes +} + +func (a *int8Array) ptr(idx int) *int8 { + p := unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(a)).Data) + return (*int8)(unsafe.Pointer(uintptr(p) + uintptr(idx))) +} + +func (a *int8Array) get(idx int) Value { + return intToValue(int64(*(a.ptr(idx)))) +} + +func (a *int8Array) getRaw(idx int) uint64 { + return uint64(*(a.ptr(idx))) +} + +func (a *int8Array) set(idx int, value Value) { + *(a.ptr(idx)) = toInt8(value) +} + +func (a *int8Array) toRaw(v Value) uint64 { + return uint64(toInt8(v)) +} + +func (a *int8Array) setRaw(idx int, v uint64) { + *(a.ptr(idx)) = int8(v) +} + +func (a *int8Array) less(i, j int) bool { + return *(a.ptr(i)) < *(a.ptr(j)) +} + +func (a *int8Array) swap(i, j int) { + pi, pj := a.ptr(i), a.ptr(j) + *pi, *pj = *pj, *pi +} + +func (a *int8Array) typeMatch(v Value) bool { + if i, ok := v.(valueInt); ok { + return i >= math.MinInt8 && i <= math.MaxInt8 + } + return false +} + +func (a *int8Array) export(offset int, length int) interface{} { + var res []int8 + sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&res)) + sliceHeader.Data = (*reflect.SliceHeader)(unsafe.Pointer(a)).Data + uintptr(offset) + sliceHeader.Len = length + sliceHeader.Cap = length + return res +} + +var typeInt8Array = reflect.TypeOf(([]int8)(nil)) + +func (a *int8Array) exportType() reflect.Type { + return typeInt8Array +} + +func (a *uint16Array) toRaw(v Value) uint64 { + return uint64(toUint16(v)) +} + +func (a *uint16Array) ptr(idx int) *uint16 { + p := unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(a)).Data) + return (*uint16)(unsafe.Pointer(uintptr(p) + uintptr(idx)*2)) +} + +func (a *uint16Array) get(idx int) Value { + return intToValue(int64(*(a.ptr(idx)))) +} + +func (a *uint16Array) set(idx int, value Value) { + *(a.ptr(idx)) = toUint16(value) +} + +func (a *uint16Array) getRaw(idx int) uint64 { + return uint64(*(a.ptr(idx))) +} + +func (a *uint16Array) setRaw(idx int, raw uint64) { + *(a.ptr(idx)) = uint16(raw) +} + +func (a *uint16Array) less(i, j int) bool { + return *(a.ptr(i)) < *(a.ptr(j)) +} + +func (a *uint16Array) swap(i, j int) { + pi, pj := a.ptr(i), a.ptr(j) + *pi, *pj = *pj, *pi +} + +func (a *uint16Array) typeMatch(v Value) bool { + if i, ok := v.(valueInt); ok { + return i >= 0 && i <= math.MaxUint16 + } + return false +} + +var typeUint16Array = reflect.TypeOf(([]uint16)(nil)) + +func (a *uint16Array) export(offset int, length int) interface{} { + var res []uint16 + sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&res)) + sliceHeader.Data = (*reflect.SliceHeader)(unsafe.Pointer(a)).Data + uintptr(offset)*2 + sliceHeader.Len = length + sliceHeader.Cap = length + return res +} + +func (a *uint16Array) exportType() reflect.Type { + return typeUint16Array +} + +func (a *int16Array) ptr(idx int) *int16 { + p := unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(a)).Data) + return (*int16)(unsafe.Pointer(uintptr(p) + uintptr(idx)*2)) +} + +func (a *int16Array) get(idx int) Value { + return intToValue(int64(*(a.ptr(idx)))) +} + +func (a *int16Array) getRaw(idx int) uint64 { + return uint64(*(a.ptr(idx))) +} + +func (a *int16Array) set(idx int, value Value) { + *(a.ptr(idx)) = toInt16(value) +} + +func (a *int16Array) toRaw(v Value) uint64 { + return uint64(toInt16(v)) +} + +func (a *int16Array) setRaw(idx int, v uint64) { + *(a.ptr(idx)) = int16(v) +} + +func (a *int16Array) less(i, j int) bool { + return *(a.ptr(i)) < *(a.ptr(j)) +} + +func (a *int16Array) swap(i, j int) { + pi, pj := a.ptr(i), a.ptr(j) + *pi, *pj = *pj, *pi +} + +func (a *int16Array) typeMatch(v Value) bool { + if i, ok := v.(valueInt); ok { + return i >= math.MinInt16 && i <= math.MaxInt16 + } + return false +} + +func (a *int16Array) export(offset int, length int) interface{} { + var res []int16 + sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&res)) + sliceHeader.Data = (*reflect.SliceHeader)(unsafe.Pointer(a)).Data + uintptr(offset)*2 + sliceHeader.Len = length + sliceHeader.Cap = length + return res +} + +var typeInt16Array = reflect.TypeOf(([]int16)(nil)) + +func (a *int16Array) exportType() reflect.Type { + return typeInt16Array +} + +func (a *uint32Array) ptr(idx int) *uint32 { + p := unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(a)).Data) + return (*uint32)(unsafe.Pointer(uintptr(p) + uintptr(idx)*4)) +} + +func (a *uint32Array) get(idx int) Value { + return intToValue(int64(*(a.ptr(idx)))) +} + +func (a *uint32Array) getRaw(idx int) uint64 { + return uint64(*(a.ptr(idx))) +} + +func (a *uint32Array) set(idx int, value Value) { + *(a.ptr(idx)) = toUint32(value) +} + +func (a *uint32Array) toRaw(v Value) uint64 { + return uint64(toUint32(v)) +} + +func (a *uint32Array) setRaw(idx int, v uint64) { + *(a.ptr(idx)) = uint32(v) +} + +func (a *uint32Array) less(i, j int) bool { + return *(a.ptr(i)) < *(a.ptr(j)) +} + +func (a *uint32Array) swap(i, j int) { + pi, pj := a.ptr(i), a.ptr(j) + *pi, *pj = *pj, *pi +} + +func (a *uint32Array) typeMatch(v Value) bool { + if i, ok := v.(valueInt); ok { + return i >= 0 && i <= math.MaxUint32 + } + return false +} + +func (a *uint32Array) export(offset int, length int) interface{} { + var res []uint32 + sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&res)) + sliceHeader.Data = (*reflect.SliceHeader)(unsafe.Pointer(a)).Data + uintptr(offset)*4 + sliceHeader.Len = length + sliceHeader.Cap = length + return res +} + +var typeUint32Array = reflect.TypeOf(([]uint32)(nil)) + +func (a *uint32Array) exportType() reflect.Type { + return typeUint32Array +} + +func (a *int32Array) ptr(idx int) *int32 { + p := unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(a)).Data) + return (*int32)(unsafe.Pointer(uintptr(p) + uintptr(idx)*4)) +} + +func (a *int32Array) get(idx int) Value { + return intToValue(int64(*(a.ptr(idx)))) +} + +func (a *int32Array) getRaw(idx int) uint64 { + return uint64(*(a.ptr(idx))) +} + +func (a *int32Array) set(idx int, value Value) { + *(a.ptr(idx)) = toInt32(value) +} + +func (a *int32Array) toRaw(v Value) uint64 { + return uint64(toInt32(v)) +} + +func (a *int32Array) setRaw(idx int, v uint64) { + *(a.ptr(idx)) = int32(v) +} + +func (a *int32Array) less(i, j int) bool { + return *(a.ptr(i)) < *(a.ptr(j)) +} + +func (a *int32Array) swap(i, j int) { + pi, pj := a.ptr(i), a.ptr(j) + *pi, *pj = *pj, *pi +} + +func (a *int32Array) typeMatch(v Value) bool { + if i, ok := v.(valueInt); ok { + return i >= math.MinInt32 && i <= math.MaxInt32 + } + return false +} + +func (a *int32Array) export(offset int, length int) interface{} { + var res []int32 + sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&res)) + sliceHeader.Data = (*reflect.SliceHeader)(unsafe.Pointer(a)).Data + uintptr(offset)*4 + sliceHeader.Len = length + sliceHeader.Cap = length + return res +} + +var typeInt32Array = reflect.TypeOf(([]int32)(nil)) + +func (a *int32Array) exportType() reflect.Type { + return typeInt32Array +} + +func (a *float32Array) ptr(idx int) *float32 { + p := unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(a)).Data) + return (*float32)(unsafe.Pointer(uintptr(p) + uintptr(idx)*4)) +} + +func (a *float32Array) get(idx int) Value { + return floatToValue(float64(*(a.ptr(idx)))) +} + +func (a *float32Array) getRaw(idx int) uint64 { + return uint64(math.Float32bits(*(a.ptr(idx)))) +} + +func (a *float32Array) set(idx int, value Value) { + *(a.ptr(idx)) = toFloat32(value) +} + +func (a *float32Array) toRaw(v Value) uint64 { + return uint64(math.Float32bits(toFloat32(v))) +} + +func (a *float32Array) setRaw(idx int, v uint64) { + *(a.ptr(idx)) = math.Float32frombits(uint32(v)) +} + +func typedFloatLess(x, y float64) bool { + xNan := math.IsNaN(x) + yNan := math.IsNaN(y) + if yNan { + return !xNan + } else if xNan { + return false + } + if x == 0 && y == 0 { // handle neg zero + return math.Signbit(x) + } + return x < y +} + +func (a *float32Array) less(i, j int) bool { + return typedFloatLess(float64(*(a.ptr(i))), float64(*(a.ptr(j)))) +} + +func (a *float32Array) swap(i, j int) { + pi, pj := a.ptr(i), a.ptr(j) + *pi, *pj = *pj, *pi +} + +func (a *float32Array) typeMatch(v Value) bool { + switch v.(type) { + case valueInt, valueFloat: + return true + } + return false +} + +func (a *float32Array) export(offset int, length int) interface{} { + var res []float32 + sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&res)) + sliceHeader.Data = (*reflect.SliceHeader)(unsafe.Pointer(a)).Data + uintptr(offset)*4 + sliceHeader.Len = length + sliceHeader.Cap = length + return res +} + +var typeFloat32Array = reflect.TypeOf(([]float32)(nil)) + +func (a *float32Array) exportType() reflect.Type { + return typeFloat32Array +} + +func (a *float64Array) ptr(idx int) *float64 { + p := unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(a)).Data) + return (*float64)(unsafe.Pointer(uintptr(p) + uintptr(idx)*8)) +} + +func (a *float64Array) get(idx int) Value { + return floatToValue(*(a.ptr(idx))) +} + +func (a *float64Array) getRaw(idx int) uint64 { + return math.Float64bits(*(a.ptr(idx))) +} + +func (a *float64Array) set(idx int, value Value) { + *(a.ptr(idx)) = value.ToFloat() +} + +func (a *float64Array) toRaw(v Value) uint64 { + return math.Float64bits(v.ToFloat()) +} + +func (a *float64Array) setRaw(idx int, v uint64) { + *(a.ptr(idx)) = math.Float64frombits(v) +} + +func (a *float64Array) less(i, j int) bool { + return typedFloatLess(*(a.ptr(i)), *(a.ptr(j))) +} + +func (a *float64Array) swap(i, j int) { + pi, pj := a.ptr(i), a.ptr(j) + *pi, *pj = *pj, *pi +} + +func (a *float64Array) typeMatch(v Value) bool { + switch v.(type) { + case valueInt, valueFloat: + return true + } + return false +} + +func (a *float64Array) export(offset int, length int) interface{} { + var res []float64 + sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&res)) + sliceHeader.Data = (*reflect.SliceHeader)(unsafe.Pointer(a)).Data + uintptr(offset)*8 + sliceHeader.Len = length + sliceHeader.Cap = length + return res +} + +var typeFloat64Array = reflect.TypeOf(([]float64)(nil)) + +func (a *float64Array) exportType() reflect.Type { + return typeFloat64Array +} + +func (a *bigInt64Array) toRaw(value Value) uint64 { + return toBigInt64(value).Uint64() +} + +func (a *bigInt64Array) ptr(idx int) *int64 { + p := unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(a)).Data) + return (*int64)(unsafe.Pointer(uintptr(p) + uintptr(idx)*8)) +} + +func (a *bigInt64Array) get(idx int) Value { + return (*valueBigInt)(big.NewInt(*a.ptr(idx))) +} + +func toBigInt64(v Value) *big.Int { + n := (*big.Int)(toBigInt(v)) + + twoTo64 := new(big.Int).Lsh(big.NewInt(1), 64) + twoTo63 := new(big.Int).Lsh(big.NewInt(1), 63) + + int64bit := new(big.Int).Mod(n, twoTo64) + if int64bit.Cmp(twoTo63) >= 0 { + return int64bit.Sub(int64bit, twoTo64) + } + return int64bit +} + +func (a *bigInt64Array) set(idx int, value Value) { + *(a.ptr(idx)) = toBigInt64(value).Int64() +} + +func (a *bigInt64Array) getRaw(idx int) uint64 { + return uint64(*a.ptr(idx)) +} + +func (a *bigInt64Array) setRaw(idx int, raw uint64) { + *(a.ptr(idx)) = int64(raw) +} + +func (a *bigInt64Array) less(i, j int) bool { + return *(a.ptr(i)) < *(a.ptr(j)) +} + +func (a *bigInt64Array) swap(i, j int) { + pi, pj := a.ptr(i), a.ptr(j) + *pi, *pj = *pj, *pi +} + +func (a *bigInt64Array) typeMatch(v Value) bool { + if _, ok := v.(*valueBigInt); ok { + return true + } + return false +} + +func (a *bigInt64Array) export(offset int, length int) interface{} { + var res []int64 + sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&res)) + sliceHeader.Data = (*reflect.SliceHeader)(unsafe.Pointer(a)).Data + uintptr(offset)*8 + sliceHeader.Len = length + sliceHeader.Cap = length + return res +} + +var typeBigInt64Array = reflect.TypeOf(([]int64)(nil)) + +func (a *bigInt64Array) exportType() reflect.Type { + return typeBigInt64Array +} + +func (a *bigUint64Array) toRaw(value Value) uint64 { + return toBigUint64(value).Uint64() +} + +func (a *bigUint64Array) ptr(idx int) *uint64 { + p := unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(a)).Data) + return (*uint64)(unsafe.Pointer(uintptr(p) + uintptr(idx)*8)) +} + +func (a *bigUint64Array) get(idx int) Value { + return (*valueBigInt)(new(big.Int).SetUint64(*a.ptr(idx))) +} + +func toBigUint64(v Value) *big.Int { + n := (*big.Int)(toBigInt(v)) + return new(big.Int).Mod(n, new(big.Int).Lsh(big.NewInt(1), 64)) +} + +func (a *bigUint64Array) set(idx int, value Value) { + *(a.ptr(idx)) = toBigUint64(value).Uint64() +} + +func (a *bigUint64Array) getRaw(idx int) uint64 { + return *a.ptr(idx) +} + +func (a *bigUint64Array) setRaw(idx int, raw uint64) { + *(a.ptr(idx)) = raw +} + +func (a *bigUint64Array) less(i, j int) bool { + return *(a.ptr(i)) < *(a.ptr(j)) +} + +func (a *bigUint64Array) swap(i, j int) { + pi, pj := a.ptr(i), a.ptr(j) + *pi, *pj = *pj, *pi +} + +func (a *bigUint64Array) typeMatch(v Value) bool { + if _, ok := v.(*valueBigInt); ok { + return true + } + return false +} + +func (a *bigUint64Array) export(offset int, length int) interface{} { + var res []uint64 + sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&res)) + sliceHeader.Data = (*reflect.SliceHeader)(unsafe.Pointer(a)).Data + uintptr(offset)*8 + sliceHeader.Len = length + sliceHeader.Cap = length + return res +} + +var typeBigUint64Array = reflect.TypeOf(([]uint64)(nil)) + +func (a *bigUint64Array) exportType() reflect.Type { + return typeBigUint64Array +} + +func (a *typedArrayObject) _getIdx(idx int) Value { + if 0 <= idx && idx < a.length { + if !a.viewedArrayBuf.ensureNotDetached(false) { + return nil + } + return a.typedArray.get(idx + a.offset) + } + return nil +} + +func (a *typedArrayObject) getOwnPropStr(name unistring.String) Value { + idx, ok := strToIntNum(name) + if ok { + v := a._getIdx(idx) + if v != nil { + return &valueProperty{ + value: v, + writable: true, + enumerable: true, + configurable: true, + } + } + return nil + } + if idx == 0 { + return nil + } + return a.baseObject.getOwnPropStr(name) +} + +func (a *typedArrayObject) getOwnPropIdx(idx valueInt) Value { + v := a._getIdx(toIntClamp(int64(idx))) + if v != nil { + return &valueProperty{ + value: v, + writable: true, + enumerable: true, + configurable: true, + } + } + return nil +} + +func (a *typedArrayObject) getStr(name unistring.String, receiver Value) Value { + idx, ok := strToIntNum(name) + if ok { + return a._getIdx(idx) + } + if idx == 0 { + return nil + } + return a.baseObject.getStr(name, receiver) +} + +func (a *typedArrayObject) getIdx(idx valueInt, receiver Value) Value { + return a._getIdx(toIntClamp(int64(idx))) +} + +func (a *typedArrayObject) isValidIntegerIndex(idx int) bool { + if a.viewedArrayBuf.ensureNotDetached(false) { + if idx >= 0 && idx < a.length { + return true + } + } + return false +} + +func (a *typedArrayObject) _putIdx(idx int, v Value) { + switch a.typedArray.(type) { + case *bigInt64Array, *bigUint64Array: + v = toBigInt(v) + default: + v = v.ToNumber() + } + if a.isValidIntegerIndex(idx) { + a.typedArray.set(idx+a.offset, v) + } +} + +func (a *typedArrayObject) _hasIdx(idx int) bool { + return a.isValidIntegerIndex(idx) +} + +func (a *typedArrayObject) setOwnStr(p unistring.String, v Value, throw bool) bool { + idx, ok := strToIntNum(p) + if ok { + a._putIdx(idx, v) + return true + } + if idx == 0 { + toNumeric(v) // make sure it throws + return true + } + return a.baseObject.setOwnStr(p, v, throw) +} + +func (a *typedArrayObject) setOwnIdx(p valueInt, v Value, throw bool) bool { + a._putIdx(toIntClamp(int64(p)), v) + return true +} + +func (a *typedArrayObject) setForeignStr(p unistring.String, v, receiver Value, throw bool) (res bool, handled bool) { + return a._setForeignStr(p, a.getOwnPropStr(p), v, receiver, throw) +} + +func (a *typedArrayObject) setForeignIdx(p valueInt, v, receiver Value, throw bool) (res bool, handled bool) { + return a._setForeignIdx(p, trueValIfPresent(a.hasOwnPropertyIdx(p)), v, receiver, throw) +} + +func (a *typedArrayObject) hasOwnPropertyStr(name unistring.String) bool { + idx, ok := strToIntNum(name) + if ok { + return a._hasIdx(idx) + } + if idx == 0 { + return false + } + return a.baseObject.hasOwnPropertyStr(name) +} + +func (a *typedArrayObject) hasOwnPropertyIdx(idx valueInt) bool { + return a._hasIdx(toIntClamp(int64(idx))) +} + +func (a *typedArrayObject) hasPropertyStr(name unistring.String) bool { + idx, ok := strToIntNum(name) + if ok { + return a._hasIdx(idx) + } + if idx == 0 { + return false + } + return a.baseObject.hasPropertyStr(name) +} + +func (a *typedArrayObject) hasPropertyIdx(idx valueInt) bool { + return a.hasOwnPropertyIdx(idx) +} + +func (a *typedArrayObject) _defineIdxProperty(idx int, desc PropertyDescriptor, throw bool) bool { + if desc.Configurable == FLAG_FALSE || desc.Enumerable == FLAG_FALSE || desc.IsAccessor() || desc.Writable == FLAG_FALSE { + a.val.runtime.typeErrorResult(throw, "Cannot redefine property: %d", idx) + return false + } + _, ok := a._defineOwnProperty(unistring.String(strconv.Itoa(idx)), a.getOwnPropIdx(valueInt(idx)), desc, throw) + if ok { + if !a.isValidIntegerIndex(idx) { + a.val.runtime.typeErrorResult(throw, "Invalid typed array index") + return false + } + a._putIdx(idx, desc.Value) + return true + } + return ok +} + +func (a *typedArrayObject) defineOwnPropertyStr(name unistring.String, desc PropertyDescriptor, throw bool) bool { + idx, ok := strToIntNum(name) + if ok { + return a._defineIdxProperty(idx, desc, throw) + } + if idx == 0 { + a.viewedArrayBuf.ensureNotDetached(throw) + a.val.runtime.typeErrorResult(throw, "Invalid typed array index") + return false + } + return a.baseObject.defineOwnPropertyStr(name, desc, throw) +} + +func (a *typedArrayObject) defineOwnPropertyIdx(name valueInt, desc PropertyDescriptor, throw bool) bool { + return a._defineIdxProperty(toIntClamp(int64(name)), desc, throw) +} + +func (a *typedArrayObject) deleteStr(name unistring.String, throw bool) bool { + idx, ok := strToIntNum(name) + if ok { + if a.isValidIntegerIndex(idx) { + a.val.runtime.typeErrorResult(throw, "Cannot delete property '%d' of %s", idx, a.val.String()) + return false + } + return true + } + if idx == 0 { + return true + } + return a.baseObject.deleteStr(name, throw) +} + +func (a *typedArrayObject) deleteIdx(idx valueInt, throw bool) bool { + if a.viewedArrayBuf.ensureNotDetached(false) && idx >= 0 && int64(idx) < int64(a.length) { + a.val.runtime.typeErrorResult(throw, "Cannot delete property '%d' of %s", idx, a.val.String()) + return false + } + + return true +} + +func (a *typedArrayObject) stringKeys(all bool, accum []Value) []Value { + if accum == nil { + accum = make([]Value, 0, a.length) + } + for i := 0; i < a.length; i++ { + accum = append(accum, asciiString(strconv.Itoa(i))) + } + return a.baseObject.stringKeys(all, accum) +} + +type typedArrayPropIter struct { + a *typedArrayObject + idx int +} + +func (i *typedArrayPropIter) next() (propIterItem, iterNextFunc) { + if i.idx < i.a.length { + name := strconv.Itoa(i.idx) + prop := i.a._getIdx(i.idx) + i.idx++ + return propIterItem{name: asciiString(name), value: prop}, i.next + } + + return i.a.baseObject.iterateStringKeys()() +} + +func (a *typedArrayObject) iterateStringKeys() iterNextFunc { + return (&typedArrayPropIter{ + a: a, + }).next +} + +func (a *typedArrayObject) exportToArrayOrSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + if typ == typeBytes { + dst.Set(reflect.ValueOf(a.viewedArrayBuf.data)) + return nil + } + return a.baseObject.exportToArrayOrSlice(dst, typ, ctx) +} + +func (a *typedArrayObject) export(_ *objectExportCtx) interface{} { + return a.typedArray.export(a.offset, a.length) +} + +func (a *typedArrayObject) exportType() reflect.Type { + return a.typedArray.exportType() +} + +func (o *dataViewObject) exportToArrayOrSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + if typ == typeBytes { + dst.Set(reflect.ValueOf(o.viewedArrayBuf.data)) + return nil + } + return o.baseObject.exportToArrayOrSlice(dst, typ, ctx) +} + +func (r *Runtime) _newTypedArrayObject(buf *arrayBufferObject, offset, length, elemSize int, defCtor *Object, arr typedArray, proto *Object) *typedArrayObject { + o := &Object{runtime: r} + a := &typedArrayObject{ + baseObject: baseObject{ + val: o, + class: classObject, + prototype: proto, + extensible: true, + }, + viewedArrayBuf: buf, + offset: offset, + length: length, + elemSize: elemSize, + defaultCtor: defCtor, + typedArray: arr, + } + o.self = a + a.init() + return a + +} + +func (r *Runtime) newUint8ArrayObject(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject { + // Note, no need to use r.getUint8Array() here or in the similar methods below, because the value is already set + // by the time they are called. + return r._newTypedArrayObject(buf, offset, length, 1, r.global.Uint8Array, (*uint8Array)(&buf.data), proto) +} + +func (r *Runtime) newUint8ClampedArrayObject(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject { + return r._newTypedArrayObject(buf, offset, length, 1, r.global.Uint8ClampedArray, (*uint8ClampedArray)(&buf.data), proto) +} + +func (r *Runtime) newInt8ArrayObject(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject { + return r._newTypedArrayObject(buf, offset, length, 1, r.global.Int8Array, (*int8Array)(&buf.data), proto) +} + +func (r *Runtime) newUint16ArrayObject(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject { + return r._newTypedArrayObject(buf, offset, length, 2, r.global.Uint16Array, (*uint16Array)(&buf.data), proto) +} + +func (r *Runtime) newInt16ArrayObject(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject { + return r._newTypedArrayObject(buf, offset, length, 2, r.global.Int16Array, (*int16Array)(&buf.data), proto) +} + +func (r *Runtime) newUint32ArrayObject(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject { + return r._newTypedArrayObject(buf, offset, length, 4, r.global.Uint32Array, (*uint32Array)(&buf.data), proto) +} + +func (r *Runtime) newInt32ArrayObject(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject { + return r._newTypedArrayObject(buf, offset, length, 4, r.global.Int32Array, (*int32Array)(&buf.data), proto) +} + +func (r *Runtime) newFloat32ArrayObject(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject { + return r._newTypedArrayObject(buf, offset, length, 4, r.global.Float32Array, (*float32Array)(&buf.data), proto) +} + +func (r *Runtime) newFloat64ArrayObject(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject { + return r._newTypedArrayObject(buf, offset, length, 8, r.global.Float64Array, (*float64Array)(&buf.data), proto) +} + +func (r *Runtime) newBigInt64ArrayObject(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject { + return r._newTypedArrayObject(buf, offset, length, 8, r.global.BigInt64Array, (*bigInt64Array)(&buf.data), proto) +} + +func (r *Runtime) newBigUint64ArrayObject(buf *arrayBufferObject, offset, length int, proto *Object) *typedArrayObject { + return r._newTypedArrayObject(buf, offset, length, 8, r.global.BigUint64Array, (*bigUint64Array)(&buf.data), proto) +} + +func (o *dataViewObject) getIdxAndByteOrder(getIdx int, littleEndianVal Value, size int) (int, byteOrder) { + o.viewedArrayBuf.ensureNotDetached(true) + if getIdx+size > o.byteLen { + panic(o.val.runtime.newError(o.val.runtime.getRangeError(), "Index %d is out of bounds", getIdx)) + } + getIdx += o.byteOffset + var bo byteOrder + if littleEndianVal != nil { + if littleEndianVal.ToBoolean() { + bo = littleEndian + } else { + bo = bigEndian + } + } else { + bo = nativeEndian + } + return getIdx, bo +} + +func (o *arrayBufferObject) ensureNotDetached(throw bool) bool { + if o.detached { + o.val.runtime.typeErrorResult(throw, "ArrayBuffer is detached") + return false + } + return true +} + +func (o *arrayBufferObject) getFloat32(idx int, byteOrder byteOrder) float32 { + return math.Float32frombits(o.getUint32(idx, byteOrder)) +} + +func (o *arrayBufferObject) setFloat32(idx int, val float32, byteOrder byteOrder) { + o.setUint32(idx, math.Float32bits(val), byteOrder) +} + +func (o *arrayBufferObject) getFloat64(idx int, byteOrder byteOrder) float64 { + return math.Float64frombits(o.getUint64(idx, byteOrder)) +} + +func (o *arrayBufferObject) setFloat64(idx int, val float64, byteOrder byteOrder) { + o.setUint64(idx, math.Float64bits(val), byteOrder) +} + +func (o *arrayBufferObject) getUint64(idx int, byteOrder byteOrder) uint64 { + var b []byte + if byteOrder == nativeEndian { + b = o.data[idx : idx+8] + } else { + b = make([]byte, 8) + d := o.data[idx : idx+8] + b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7] = d[7], d[6], d[5], d[4], d[3], d[2], d[1], d[0] + } + return *((*uint64)(unsafe.Pointer(&b[0]))) +} + +func (o *arrayBufferObject) setUint64(idx int, val uint64, byteOrder byteOrder) { + if byteOrder == nativeEndian { + *(*uint64)(unsafe.Pointer(&o.data[idx])) = val + } else { + b := (*[8]byte)(unsafe.Pointer(&val)) + d := o.data[idx : idx+8] + d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7] = b[7], b[6], b[5], b[4], b[3], b[2], b[1], b[0] + } +} + +func (o *arrayBufferObject) getUint32(idx int, byteOrder byteOrder) uint32 { + var b []byte + if byteOrder == nativeEndian { + b = o.data[idx : idx+4] + } else { + b = make([]byte, 4) + d := o.data[idx : idx+4] + b[0], b[1], b[2], b[3] = d[3], d[2], d[1], d[0] + } + return *((*uint32)(unsafe.Pointer(&b[0]))) +} + +func (o *arrayBufferObject) setUint32(idx int, val uint32, byteOrder byteOrder) { + o.ensureNotDetached(true) + if byteOrder == nativeEndian { + *(*uint32)(unsafe.Pointer(&o.data[idx])) = val + } else { + b := (*[4]byte)(unsafe.Pointer(&val)) + d := o.data[idx : idx+4] + d[0], d[1], d[2], d[3] = b[3], b[2], b[1], b[0] + } +} + +func (o *arrayBufferObject) getBigInt64(idx int, byteOrder byteOrder) *big.Int { + var b []byte + if byteOrder == nativeEndian { + b = o.data[idx : idx+8] + } else { + b = make([]byte, 8) + d := o.data[idx : idx+8] + b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7] = d[7], d[6], d[5], d[4], d[3], d[2], d[1], d[0] + } + return big.NewInt(*((*int64)(unsafe.Pointer(&b[0])))) +} + +func (o *arrayBufferObject) getBigUint64(idx int, byteOrder byteOrder) *big.Int { + var b []byte + if byteOrder == nativeEndian { + b = o.data[idx : idx+8] + } else { + b = make([]byte, 8) + d := o.data[idx : idx+8] + b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7] = d[7], d[6], d[5], d[4], d[3], d[2], d[1], d[0] + } + return new(big.Int).SetUint64(*((*uint64)(unsafe.Pointer(&b[0])))) +} + +func (o *arrayBufferObject) setBigInt64(idx int, val *big.Int, byteOrder byteOrder) { + if byteOrder == nativeEndian { + *(*int64)(unsafe.Pointer(&o.data[idx])) = val.Int64() + } else { + n := val.Int64() + b := (*[8]byte)(unsafe.Pointer(&n)) + d := o.data[idx : idx+8] + d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7] = b[7], b[6], b[5], b[4], b[3], b[2], b[1], b[0] + } +} + +func (o *arrayBufferObject) setBigUint64(idx int, val *big.Int, byteOrder byteOrder) { + if byteOrder == nativeEndian { + *(*uint64)(unsafe.Pointer(&o.data[idx])) = val.Uint64() + } else { + n := val.Uint64() + b := (*[8]byte)(unsafe.Pointer(&n)) + d := o.data[idx : idx+8] + d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7] = b[7], b[6], b[5], b[4], b[3], b[2], b[1], b[0] + } +} + +func (o *arrayBufferObject) getUint16(idx int, byteOrder byteOrder) uint16 { + var b []byte + if byteOrder == nativeEndian { + b = o.data[idx : idx+2] + } else { + b = make([]byte, 2) + d := o.data[idx : idx+2] + b[0], b[1] = d[1], d[0] + } + return *((*uint16)(unsafe.Pointer(&b[0]))) +} + +func (o *arrayBufferObject) setUint16(idx int, val uint16, byteOrder byteOrder) { + if byteOrder == nativeEndian { + *(*uint16)(unsafe.Pointer(&o.data[idx])) = val + } else { + b := (*[2]byte)(unsafe.Pointer(&val)) + d := o.data[idx : idx+2] + d[0], d[1] = b[1], b[0] + } +} + +func (o *arrayBufferObject) getUint8(idx int) uint8 { + return o.data[idx] +} + +func (o *arrayBufferObject) setUint8(idx int, val uint8) { + o.data[idx] = val +} + +func (o *arrayBufferObject) getInt32(idx int, byteOrder byteOrder) int32 { + return int32(o.getUint32(idx, byteOrder)) +} + +func (o *arrayBufferObject) setInt32(idx int, val int32, byteOrder byteOrder) { + o.setUint32(idx, uint32(val), byteOrder) +} + +func (o *arrayBufferObject) getInt16(idx int, byteOrder byteOrder) int16 { + return int16(o.getUint16(idx, byteOrder)) +} + +func (o *arrayBufferObject) setInt16(idx int, val int16, byteOrder byteOrder) { + o.setUint16(idx, uint16(val), byteOrder) +} + +func (o *arrayBufferObject) getInt8(idx int) int8 { + return int8(o.data[idx]) +} + +func (o *arrayBufferObject) setInt8(idx int, val int8) { + o.setUint8(idx, uint8(val)) +} + +func (o *arrayBufferObject) detach() { + o.data = nil + o.detached = true +} + +func (o *arrayBufferObject) exportType() reflect.Type { + return arrayBufferType +} + +func (o *arrayBufferObject) export(*objectExportCtx) interface{} { + return ArrayBuffer{ + buf: o, + } +} + +func (o *arrayBufferObject) exportToArrayOrSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + if typ == typeBytes { + dst.Set(reflect.ValueOf(o.data)) + return nil + } + return o.baseObject.exportToArrayOrSlice(dst, typ, ctx) +} + +func (r *Runtime) _newArrayBuffer(proto *Object, o *Object) *arrayBufferObject { + if o == nil { + o = &Object{runtime: r} + } + b := &arrayBufferObject{ + baseObject: baseObject{ + class: classObject, + val: o, + prototype: proto, + extensible: true, + }, + } + o.self = b + b.init() + return b +} + +func init() { + buf := [2]byte{} + *(*uint16)(unsafe.Pointer(&buf[0])) = uint16(0xCAFE) + + switch buf { + case [2]byte{0xFE, 0xCA}: + nativeEndian = littleEndian + case [2]byte{0xCA, 0xFE}: + nativeEndian = bigEndian + default: + panic("Could not determine native endianness.") + } +} diff --git a/goja/typedarrays_test.go b/goja/typedarrays_test.go new file mode 100644 index 0000000..92653d6 --- /dev/null +++ b/goja/typedarrays_test.go @@ -0,0 +1,544 @@ +package goja + +import ( + "bytes" + "testing" +) + +func TestUint16ArrayObject(t *testing.T) { + vm := New() + buf := vm._newArrayBuffer(vm.global.ArrayBufferPrototype, nil) + buf.data = make([]byte, 16) + if nativeEndian == littleEndian { + buf.data[2] = 0xFE + buf.data[3] = 0xCA + } else { + buf.data[2] = 0xCA + buf.data[3] = 0xFE + } + a := vm.newUint16ArrayObject(buf, 1, 1, nil) + v := a.getIdx(valueInt(0), nil) + if v != valueInt(0xCAFE) { + t.Fatalf("v: %v", v) + } +} + +func TestArrayBufferGoWrapper(t *testing.T) { + vm := New() + data := []byte{0xAA, 0xBB} + buf := vm.NewArrayBuffer(data) + vm.Set("buf", buf) + _, err := vm.RunString(` + var a = new Uint8Array(buf); + if (a.length !== 2 || a[0] !== 0xAA || a[1] !== 0xBB) { + throw new Error(a); + } + `) + if err != nil { + t.Fatal(err) + } + ret, err := vm.RunString(` + var b = Uint8Array.of(0xCC, 0xDD); + b.buffer; + `) + if err != nil { + t.Fatal(err) + } + buf1 := ret.Export().(ArrayBuffer) + data1 := buf1.Bytes() + if len(data1) != 2 || data1[0] != 0xCC || data1[1] != 0xDD { + t.Fatal(data1) + } + if buf1.Detached() { + t.Fatal("buf1.Detached() returned true") + } + if !buf1.Detach() { + t.Fatal("buf1.Detach() returned false") + } + if !buf1.Detached() { + t.Fatal("buf1.Detached() returned false") + } + _, err = vm.RunString(` + if (b[0] !== undefined) { + throw new Error("b[0] !== undefined"); + } + `) + if err != nil { + t.Fatal(err) + } +} + +func TestTypedArrayIdx(t *testing.T) { + const SCRIPT = ` + var a = new Uint8Array(1); + + // 32-bit integer overflow, should not panic on 32-bit architectures + if (a[4294967297] !== undefined) { + throw new Error("4294967297"); + } + + // Canonical non-integer + a["Infinity"] = 8; + if (a["Infinity"] !== undefined) { + throw new Error("Infinity"); + } + a["NaN"] = 1; + if (a["NaN"] !== undefined) { + throw new Error("NaN"); + } + + // Non-canonical integer + a["00"] = "00"; + if (a["00"] !== "00") { + throw new Error("00"); + } + + // Non-canonical non-integer + a["1e-3"] = "1e-3"; + if (a["1e-3"] !== "1e-3") { + throw new Error("1e-3"); + } + if (a["0.001"] !== undefined) { + throw new Error("0.001"); + } + + // Negative zero + a["-0"] = 88; + if (a["-0"] !== undefined) { + throw new Error("-0"); + } + + if (a[0] !== 0) { + throw new Error("0"); + } + + a["9007199254740992"] = 1; + if (a["9007199254740992"] !== undefined) { + throw new Error("9007199254740992"); + } + a["-9007199254740992"] = 1; + if (a["-9007199254740992"] !== undefined) { + throw new Error("-9007199254740992"); + } + + // Safe integer overflow, not canonical (Number("9007199254740993") === 9007199254740992) + a["9007199254740993"] = 1; + if (a["9007199254740993"] !== 1) { + throw new Error("9007199254740993"); + } + a["-9007199254740993"] = 1; + if (a["-9007199254740993"] !== 1) { + throw new Error("-9007199254740993"); + } + + // Safe integer overflow, canonical Number("9007199254740994") == 9007199254740994 + a["9007199254740994"] = 1; + if (a["9007199254740994"] !== undefined) { + throw new Error("9007199254740994"); + } + a["-9007199254740994"] = 1; + if (a["-9007199254740994"] !== undefined) { + throw new Error("-9007199254740994"); + } + ` + + testScript(SCRIPT, _undefined, t) +} + +func TestTypedArraySetDetachedBuffer(t *testing.T) { + const SCRIPT = ` + let sample = new Uint8Array([42]); + $DETACHBUFFER(sample.buffer); + sample[0] = 1; + + assert.sameValue(sample[0], undefined, 'sample[0] = 1 is undefined'); + sample['1.1'] = 1; + assert.sameValue(sample['1.1'], undefined, 'sample[\'1.1\'] = 1 is undefined'); + sample['-0'] = 1; + assert.sameValue(sample['-0'], undefined, 'sample[\'-0\'] = 1 is undefined'); + sample['-1'] = 1; + assert.sameValue(sample['-1'], undefined, 'sample[\'-1\'] = 1 is undefined'); + sample['1'] = 1; + assert.sameValue(sample['1'], undefined, 'sample[\'1\'] = 1 is undefined'); + sample['2'] = 1; + assert.sameValue(sample['2'], undefined, 'sample[\'2\'] = 1 is undefined'); + ` + vm := New() + vm.Set("$DETACHBUFFER", func(buf *ArrayBuffer) { + buf.Detach() + }) + vm.testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestTypedArrayDefinePropDetachedBuffer(t *testing.T) { + const SCRIPT = ` + var desc = { + value: 0, + configurable: false, + enumerable: true, + writable: true + }; + + var obj = { + valueOf: function() { + throw new Error("valueOf() was called"); + } + }; + let sample = new Uint8Array(42); + $DETACHBUFFER(sample.buffer); + + assert.sameValue( + Reflect.defineProperty(sample, "0", desc), + false, + 'Reflect.defineProperty(sample, "0", {value: 0, configurable: false, enumerable: true, writable: true} ) must return false' + ); + + assert.sameValue( + Reflect.defineProperty(sample, "-1", desc), + false, + 'Reflect.defineProperty(sample, "-1", {value: 0, configurable: false, enumerable: true, writable: true} ) must return false' + ); + + assert.sameValue( + Reflect.defineProperty(sample, "1.1", desc), + false, + 'Reflect.defineProperty(sample, "1.1", {value: 0, configurable: false, enumerable: true, writable: true} ) must return false' + ); + + assert.sameValue( + Reflect.defineProperty(sample, "-0", desc), + false, + 'Reflect.defineProperty(sample, "-0", {value: 0, configurable: false, enumerable: true, writable: true} ) must return false' + ); + + assert.sameValue( + Reflect.defineProperty(sample, "2", { + configurable: true, + enumerable: true, + writable: true, + value: obj + }), + false, + 'Reflect.defineProperty(sample, "2", {configurable: true, enumerable: true, writable: true, value: obj}) must return false' + ); + + assert.sameValue( + Reflect.defineProperty(sample, "3", { + configurable: false, + enumerable: false, + writable: true, + value: obj + }), + false, + 'Reflect.defineProperty(sample, "3", {configurable: false, enumerable: false, writable: true, value: obj}) must return false' + ); + + assert.sameValue( + Reflect.defineProperty(sample, "4", { + writable: false, + configurable: false, + enumerable: true, + value: obj + }), + false, + 'Reflect.defineProperty("new TA(42)", "4", {writable: false, configurable: false, enumerable: true, value: obj}) must return false' + ); + + assert.sameValue( + Reflect.defineProperty(sample, "42", desc), + false, + 'Reflect.defineProperty(sample, "42", {value: 0, configurable: false, enumerable: true, writable: true} ) must return false' + ); + + assert.sameValue( + Reflect.defineProperty(sample, "43", desc), + false, + 'Reflect.defineProperty(sample, "43", {value: 0, configurable: false, enumerable: true, writable: true} ) must return false' + ); + + assert.sameValue( + Reflect.defineProperty(sample, "5", { + get: function() {} + }), + false, + 'Reflect.defineProperty(sample, "5", {get: function() {}}) must return false' + ); + + assert.sameValue( + Reflect.defineProperty(sample, "6", { + configurable: false, + enumerable: true, + writable: true + }), + false, + 'Reflect.defineProperty(sample, "6", {configurable: false, enumerable: true, writable: true}) must return false' + ); + ` + vm := New() + vm.Set("$DETACHBUFFER", func(buf *ArrayBuffer) { + buf.Detach() + }) + vm.testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestTypedArrayDefineProperty(t *testing.T) { + const SCRIPT = ` + var a = new Uint8Array(1); + + assert.throws(TypeError, function() { + Object.defineProperty(a, "1", {value: 1}); + }); + assert.sameValue(Reflect.defineProperty(a, "1", {value: 1}), false, "1"); + + assert.throws(TypeError, function() { + Object.defineProperty(a, "Infinity", {value: 8}); + }); + assert.sameValue(Reflect.defineProperty(a, "Infinity", {value: 8}), false, "Infinity"); + + Object.defineProperty(a, "test", {value: "passed"}); + assert.sameValue(a.test, "passed", "string property"); + + assert.throws(TypeError, function() { + Object.defineProperty(a, "0", {value: 1, writable: false}); + }, "define non-writable"); + + assert.throws(TypeError, function() { + Object.defineProperty(a, "0", {get() { return 1; }}); + }, "define accessor"); + + var sample = new Uint8Array([42, 42]); + + assert.sameValue( + Reflect.defineProperty(sample, "0", { + value: 8, + configurable: true, + enumerable: true, + writable: true + }), + true + ); + + assert.sameValue(sample[0], 8, "property value was set"); + let descriptor0 = Object.getOwnPropertyDescriptor(sample, "0"); + assert.sameValue(descriptor0.value, 8); + assert.sameValue(descriptor0.configurable, true, "configurable"); + assert.sameValue(descriptor0.enumerable, true); + assert.sameValue(descriptor0.writable, true); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestTypedArrayGetInvalidIndex(t *testing.T) { + const SCRIPT = ` + var TypedArray = Object.getPrototypeOf(Int8Array); + var proto = TypedArray.prototype; + Object.defineProperty(proto, "1", { + get: function() { + throw new Error("OrdinaryGet was called!"); + } + }); + var a = new Uint8Array(1); + assert.sameValue(a[1], undefined); + assert.sameValue(a["1"], undefined); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func TestExportArrayBufferToBytes(t *testing.T) { + vm := New() + bb := []byte("test") + ab := vm.NewArrayBuffer(bb) + var b []byte + err := vm.ExportTo(vm.ToValue(ab), &b) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(b, bb) { + t.Fatal("Not equal") + } + + err = vm.ExportTo(vm.ToValue(123), &b) + if err == nil { + t.Fatal("expected error") + } +} + +func TestTypedArrayExport(t *testing.T) { + vm := New() + + t.Run("uint8", func(t *testing.T) { + v, err := vm.RunString("new Uint8Array([1, 2])") + if err != nil { + t.Fatal(err) + } + if a, ok := v.Export().([]uint8); ok { + if len(a) != 2 || a[0] != 1 || a[1] != 2 { + t.Fatal(a) + } + } else { + t.Fatal("Wrong export type") + } + _, err = vm.RunString(`{ + let a = new Uint8Array([1, 2]); + if (a[0] !== 1 || a[1] !== 2) { + throw new Error(a); + } + }`) + if err != nil { + t.Fatal(err) + } + }) + + t.Run("uint8-slice", func(t *testing.T) { + v, err := vm.RunString(`{ + const buf = new Uint8Array([1, 2]).buffer; + new Uint8Array(buf, 1, 1); + }`) + if err != nil { + t.Fatal(err) + } + if a, ok := v.Export().([]uint8); ok { + if len(a) != 1 || a[0] != 2 { + t.Fatal(a) + } + } else { + t.Fatal("Wrong export type") + } + _, err = vm.RunString(`{ + let a = new Uint8Array([1, 2]); + if (a[0] !== 1 || a[1] !== 2) { + throw new Error(a); + } + }`) + if err != nil { + t.Fatal(err) + } + }) + + t.Run("int8", func(t *testing.T) { + v, err := vm.RunString("new Int8Array([1, -2])") + if err != nil { + t.Fatal(err) + } + if a, ok := v.Export().([]int8); ok { + if len(a) != 2 || a[0] != 1 || a[1] != -2 { + t.Fatal(a) + } + } else { + t.Fatal("Wrong export type") + } + }) + + t.Run("uint16", func(t *testing.T) { + v, err := vm.RunString("new Uint16Array([1, 63000])") + if err != nil { + t.Fatal(err) + } + if a, ok := v.Export().([]uint16); ok { + if len(a) != 2 || a[0] != 1 || a[1] != 63000 { + t.Fatal(a) + } + } else { + t.Fatal("Wrong export type") + } + }) + + t.Run("int16", func(t *testing.T) { + v, err := vm.RunString("new Int16Array([1, -31000])") + if err != nil { + t.Fatal(err) + } + if a, ok := v.Export().([]int16); ok { + if len(a) != 2 || a[0] != 1 || a[1] != -31000 { + t.Fatal(a) + } + } else { + t.Fatal("Wrong export type") + } + }) + + t.Run("uint32", func(t *testing.T) { + v, err := vm.RunString("new Uint32Array([1, 123456])") + if err != nil { + t.Fatal(err) + } + if a, ok := v.Export().([]uint32); ok { + if len(a) != 2 || a[0] != 1 || a[1] != 123456 { + t.Fatal(a) + } + } else { + t.Fatal("Wrong export type") + } + }) + + t.Run("int32", func(t *testing.T) { + v, err := vm.RunString("new Int32Array([1, -123456])") + if err != nil { + t.Fatal(err) + } + if a, ok := v.Export().([]int32); ok { + if len(a) != 2 || a[0] != 1 || a[1] != -123456 { + t.Fatal(a) + } + } else { + t.Fatal("Wrong export type") + } + }) + + t.Run("float32", func(t *testing.T) { + v, err := vm.RunString("new Float32Array([1, -1.23456])") + if err != nil { + t.Fatal(err) + } + if a, ok := v.Export().([]float32); ok { + if len(a) != 2 || a[0] != 1 || a[1] != -1.23456 { + t.Fatal(a) + } + } else { + t.Fatal("Wrong export type") + } + }) + + t.Run("float64", func(t *testing.T) { + v, err := vm.RunString("new Float64Array([1, -1.23456789])") + if err != nil { + t.Fatal(err) + } + if a, ok := v.Export().([]float64); ok { + if len(a) != 2 || a[0] != 1 || a[1] != -1.23456789 { + t.Fatal(a) + } + } else { + t.Fatal("Wrong export type") + } + }) + + t.Run("bigint64", func(t *testing.T) { + v, err := vm.RunString("new BigInt64Array([18446744073709551617n, 2n])") + if err != nil { + t.Fatal(err) + } + if a, ok := v.Export().([]int64); ok { + if len(a) != 2 || a[0] != 1 || a[1] != 2 { + t.Fatal(a) + } + } else { + t.Fatal("Wrong export type") + } + }) + + t.Run("biguint64", func(t *testing.T) { + v, err := vm.RunString("new BigUint64Array([18446744073709551617n, 2n])") + if err != nil { + t.Fatal(err) + } + if a, ok := v.Export().([]uint64); ok { + if len(a) != 2 || a[0] != 1 || a[1] != 2 { + t.Fatal(a) + } + } else { + t.Fatal("Wrong export type") + } + }) + +} diff --git a/goja/unistring/string.go b/goja/unistring/string.go new file mode 100644 index 0000000..4628299 --- /dev/null +++ b/goja/unistring/string.go @@ -0,0 +1,137 @@ +// Package unistring contains an implementation of a hybrid ASCII/UTF-16 string. +// For ASCII strings the underlying representation is equivalent to a normal Go string. +// For unicode strings the underlying representation is UTF-16 as []uint16 with 0th element set to 0xFEFF. +// unicode.String allows representing malformed UTF-16 values (e.g. stand-alone parts of surrogate pairs) +// which cannot be represented in UTF-8. +// At the same time it is possible to use unicode.String as property keys just as efficiently as simple strings, +// (the leading 0xFEFF ensures there is no clash with ASCII string), and it is possible to convert it +// to valueString without extra allocations. +package unistring + +import ( + "reflect" + "unicode/utf16" + "unicode/utf8" + "unsafe" +) + +const ( + BOM = 0xFEFF +) + +type String string + +// Scan checks if the string contains any unicode characters. If it does, converts to an array suitable for creating +// a String using FromUtf16, otherwise returns nil. +func Scan(s string) []uint16 { + utf16Size := 0 + for ; utf16Size < len(s); utf16Size++ { + if s[utf16Size] >= utf8.RuneSelf { + goto unicode + } + } + return nil +unicode: + for _, chr := range s[utf16Size:] { + utf16Size++ + if chr > 0xFFFF { + utf16Size++ + } + } + + buf := make([]uint16, utf16Size+1) + buf[0] = BOM + c := 1 + for _, chr := range s { + if chr <= 0xFFFF { + buf[c] = uint16(chr) + } else { + first, second := utf16.EncodeRune(chr) + buf[c] = uint16(first) + c++ + buf[c] = uint16(second) + } + c++ + } + + return buf +} + +func NewFromString(s string) String { + if buf := Scan(s); buf != nil { + return FromUtf16(buf) + } + return String(s) +} + +func NewFromRunes(s []rune) String { + ascii := true + size := 0 + for _, c := range s { + if c >= utf8.RuneSelf { + ascii = false + if c > 0xFFFF { + size++ + } + } + size++ + } + if ascii { + return String(s) + } + b := make([]uint16, size+1) + b[0] = BOM + i := 1 + for _, c := range s { + if c <= 0xFFFF { + b[i] = uint16(c) + } else { + first, second := utf16.EncodeRune(c) + b[i] = uint16(first) + i++ + b[i] = uint16(second) + } + i++ + } + return FromUtf16(b) +} + +func FromUtf16(b []uint16) String { + var str string + hdr := (*reflect.StringHeader)(unsafe.Pointer(&str)) + hdr.Data = uintptr(unsafe.Pointer(&b[0])) + hdr.Len = len(b) * 2 + + return String(str) +} + +func (s String) String() string { + if b := s.AsUtf16(); b != nil { + return string(utf16.Decode(b[1:])) + } + + return string(s) +} + +func (s String) AsUtf16() []uint16 { + if len(s) < 4 || len(s)&1 != 0 { + return nil + } + + var a []uint16 + raw := string(s) + + sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&a)) + sliceHeader.Data = (*reflect.StringHeader)(unsafe.Pointer(&raw)).Data + + l := len(raw) / 2 + + sliceHeader.Len = l + sliceHeader.Cap = l + + if a[0] == BOM { + return a + } + + return nil +} diff --git a/goja/unistring/string_test.go b/goja/unistring/string_test.go new file mode 100644 index 0000000..2e4f420 --- /dev/null +++ b/goja/unistring/string_test.go @@ -0,0 +1,16 @@ +package unistring + +import "testing" + +func TestString_AsUtf16(t *testing.T) { + const str = "más" + s := NewFromString(str) + + if b := s.AsUtf16(); len(b) != 4 || b[0] != BOM { + t.Fatal(b) + } + + if s.String() != str { + t.Fatal(s) + } +} diff --git a/goja/value.go b/goja/value.go new file mode 100644 index 0000000..6ca3d72 --- /dev/null +++ b/goja/value.go @@ -0,0 +1,1196 @@ +package goja + +import ( + "fmt" + "hash/maphash" + "math" + "math/big" + "reflect" + "strconv" + "unsafe" + + "github.com/dop251/goja/ftoa" + "github.com/dop251/goja/unistring" +) + +var ( + // Not goroutine-safe, do not use for anything other than package level init + pkgHasher maphash.Hash + + hashFalse = randomHash() + hashTrue = randomHash() + hashNull = randomHash() + hashUndef = randomHash() +) + +// Not goroutine-safe, do not use for anything other than package level init +func randomHash() uint64 { + pkgHasher.WriteByte(0) + return pkgHasher.Sum64() +} + +var ( + valueFalse Value = valueBool(false) + valueTrue Value = valueBool(true) + _null Value = valueNull{} + _NaN Value = valueFloat(math.NaN()) + _positiveInf Value = valueFloat(math.Inf(+1)) + _negativeInf Value = valueFloat(math.Inf(-1)) + _positiveZero Value = valueInt(0) + negativeZero = math.Float64frombits(0 | (1 << 63)) + _negativeZero Value = valueFloat(negativeZero) + _epsilon = valueFloat(2.2204460492503130808472633361816e-16) + _undefined Value = valueUndefined{} +) + +var ( + reflectTypeInt = reflect.TypeOf(int64(0)) + reflectTypeBool = reflect.TypeOf(false) + reflectTypeNil = reflect.TypeOf(nil) + reflectTypeFloat = reflect.TypeOf(float64(0)) + reflectTypeMap = reflect.TypeOf(map[string]interface{}{}) + reflectTypeArray = reflect.TypeOf([]interface{}{}) + reflectTypeArrayPtr = reflect.TypeOf((*[]interface{})(nil)) + reflectTypeString = reflect.TypeOf("") + reflectTypeFunc = reflect.TypeOf((func(FunctionCall) Value)(nil)) + reflectTypeError = reflect.TypeOf((*error)(nil)).Elem() +) + +var intCache [256]Value + +// Value represents an ECMAScript value. +// +// Export returns a "plain" Go value which type depends on the type of the Value. +// +// For integer numbers it's int64. +// +// For any other numbers (including Infinities, NaN and negative zero) it's float64. +// +// For string it's a string. Note that unicode strings are converted into UTF-8 with invalid code points replaced with utf8.RuneError. +// +// For boolean it's bool. +// +// For null and undefined it's nil. +// +// For Object it depends on the Object type, see Object.Export() for more details. +type Value interface { + ToInteger() int64 + toString() String + string() unistring.String + ToString() Value + String() string + ToFloat() float64 + ToNumber() Value + ToBoolean() bool + ToObject(*Runtime) *Object + SameAs(Value) bool + Equals(Value) bool + StrictEquals(Value) bool + Export() interface{} + ExportType() reflect.Type + + baseObject(r *Runtime) *Object + + hash(hasher *maphash.Hash) uint64 +} + +type valueContainer interface { + toValue(*Runtime) Value +} + +type typeError string +type rangeError string +type referenceError string +type syntaxError string + +type valueInt int64 +type valueFloat float64 +type valueBool bool +type valueNull struct{} +type valueUndefined struct { + valueNull +} + +// *Symbol is a Value containing ECMAScript Symbol primitive. Symbols must only be created +// using NewSymbol(). Zero values and copying of values (i.e. *s1 = *s2) are not permitted. +// Well-known Symbols can be accessed using Sym* package variables (SymIterator, etc...) +// Symbols can be shared by multiple Runtimes. +type Symbol struct { + h uintptr + desc String +} + +type valueUnresolved struct { + r *Runtime + ref unistring.String +} + +type memberUnresolved struct { + valueUnresolved +} + +type valueProperty struct { + value Value + writable bool + configurable bool + enumerable bool + accessor bool + getterFunc *Object + setterFunc *Object +} + +var ( + errAccessBeforeInit = referenceError("Cannot access a variable before initialization") + errAssignToConst = typeError("Assignment to constant variable.") + errMixBigIntType = typeError("Cannot mix BigInt and other types, use explicit conversions") +) + +func propGetter(o Value, v Value, r *Runtime) *Object { + if v == _undefined { + return nil + } + if obj, ok := v.(*Object); ok { + if _, ok := obj.self.assertCallable(); ok { + return obj + } + } + r.typeErrorResult(true, "Getter must be a function: %s", v.toString()) + return nil +} + +func propSetter(o Value, v Value, r *Runtime) *Object { + if v == _undefined { + return nil + } + if obj, ok := v.(*Object); ok { + if _, ok := obj.self.assertCallable(); ok { + return obj + } + } + r.typeErrorResult(true, "Setter must be a function: %s", v.toString()) + return nil +} + +func fToStr(num float64, mode ftoa.FToStrMode, prec int) string { + var buf1 [128]byte + return string(ftoa.FToStr(num, mode, prec, buf1[:0])) +} + +func (i valueInt) ToInteger() int64 { + return int64(i) +} + +func (i valueInt) toString() String { + return asciiString(i.String()) +} + +func (i valueInt) string() unistring.String { + return unistring.String(i.String()) +} + +func (i valueInt) ToString() Value { + return i +} + +func (i valueInt) String() string { + return strconv.FormatInt(int64(i), 10) +} + +func (i valueInt) ToFloat() float64 { + return float64(i) +} + +func (i valueInt) ToBoolean() bool { + return i != 0 +} + +func (i valueInt) ToObject(r *Runtime) *Object { + return r.newPrimitiveObject(i, r.getNumberPrototype(), classNumber) +} + +func (i valueInt) ToNumber() Value { + return i +} + +func (i valueInt) SameAs(other Value) bool { + return i == other +} + +func (i valueInt) Equals(other Value) bool { + switch o := other.(type) { + case valueInt: + return i == o + case *valueBigInt: + return (*big.Int)(o).Cmp(big.NewInt(int64(i))) == 0 + case valueFloat: + return float64(i) == float64(o) + case String: + return o.ToNumber().Equals(i) + case valueBool: + return int64(i) == o.ToInteger() + case *Object: + return i.Equals(o.toPrimitive()) + } + + return false +} + +func (i valueInt) StrictEquals(other Value) bool { + switch o := other.(type) { + case valueInt: + return i == o + case valueFloat: + return float64(i) == float64(o) + } + + return false +} + +func (i valueInt) baseObject(r *Runtime) *Object { + return r.getNumberPrototype() +} + +func (i valueInt) Export() interface{} { + return int64(i) +} + +func (i valueInt) ExportType() reflect.Type { + return reflectTypeInt +} + +func (i valueInt) hash(*maphash.Hash) uint64 { + return uint64(i) +} + +func (b valueBool) ToInteger() int64 { + if b { + return 1 + } + return 0 +} + +func (b valueBool) toString() String { + if b { + return stringTrue + } + return stringFalse +} + +func (b valueBool) ToString() Value { + return b +} + +func (b valueBool) String() string { + if b { + return "true" + } + return "false" +} + +func (b valueBool) string() unistring.String { + return unistring.String(b.String()) +} + +func (b valueBool) ToFloat() float64 { + if b { + return 1.0 + } + return 0 +} + +func (b valueBool) ToBoolean() bool { + return bool(b) +} + +func (b valueBool) ToObject(r *Runtime) *Object { + return r.newPrimitiveObject(b, r.getBooleanPrototype(), "Boolean") +} + +func (b valueBool) ToNumber() Value { + if b { + return valueInt(1) + } + return valueInt(0) +} + +func (b valueBool) SameAs(other Value) bool { + if other, ok := other.(valueBool); ok { + return b == other + } + return false +} + +func (b valueBool) Equals(other Value) bool { + if o, ok := other.(valueBool); ok { + return b == o + } + + if b { + return other.Equals(intToValue(1)) + } else { + return other.Equals(intToValue(0)) + } + +} + +func (b valueBool) StrictEquals(other Value) bool { + if other, ok := other.(valueBool); ok { + return b == other + } + return false +} + +func (b valueBool) baseObject(r *Runtime) *Object { + return r.getBooleanPrototype() +} + +func (b valueBool) Export() interface{} { + return bool(b) +} + +func (b valueBool) ExportType() reflect.Type { + return reflectTypeBool +} + +func (b valueBool) hash(*maphash.Hash) uint64 { + if b { + return hashTrue + } + + return hashFalse +} + +func (n valueNull) ToInteger() int64 { + return 0 +} + +func (n valueNull) toString() String { + return stringNull +} + +func (n valueNull) string() unistring.String { + return stringNull.string() +} + +func (n valueNull) ToString() Value { + return n +} + +func (n valueNull) String() string { + return "null" +} + +func (u valueUndefined) toString() String { + return stringUndefined +} + +func (u valueUndefined) ToString() Value { + return u +} + +func (u valueUndefined) String() string { + return "undefined" +} + +func (u valueUndefined) string() unistring.String { + return "undefined" +} + +func (u valueUndefined) ToNumber() Value { + return _NaN +} + +func (u valueUndefined) SameAs(other Value) bool { + _, same := other.(valueUndefined) + return same +} + +func (u valueUndefined) StrictEquals(other Value) bool { + _, same := other.(valueUndefined) + return same +} + +func (u valueUndefined) ToFloat() float64 { + return math.NaN() +} + +func (u valueUndefined) hash(*maphash.Hash) uint64 { + return hashUndef +} + +func (n valueNull) ToFloat() float64 { + return 0 +} + +func (n valueNull) ToBoolean() bool { + return false +} + +func (n valueNull) ToObject(r *Runtime) *Object { + r.typeErrorResult(true, "Cannot convert undefined or null to object") + return nil + //return r.newObject() +} + +func (n valueNull) ToNumber() Value { + return intToValue(0) +} + +func (n valueNull) SameAs(other Value) bool { + _, same := other.(valueNull) + return same +} + +func (n valueNull) Equals(other Value) bool { + switch other.(type) { + case valueUndefined, valueNull: + return true + } + return false +} + +func (n valueNull) StrictEquals(other Value) bool { + _, same := other.(valueNull) + return same +} + +func (n valueNull) baseObject(*Runtime) *Object { + return nil +} + +func (n valueNull) Export() interface{} { + return nil +} + +func (n valueNull) ExportType() reflect.Type { + return reflectTypeNil +} + +func (n valueNull) hash(*maphash.Hash) uint64 { + return hashNull +} + +func (p *valueProperty) ToInteger() int64 { + return 0 +} + +func (p *valueProperty) toString() String { + return stringEmpty +} + +func (p *valueProperty) string() unistring.String { + return "" +} + +func (p *valueProperty) ToString() Value { + return _undefined +} + +func (p *valueProperty) String() string { + return "" +} + +func (p *valueProperty) ToFloat() float64 { + return math.NaN() +} + +func (p *valueProperty) ToBoolean() bool { + return false +} + +func (p *valueProperty) ToObject(*Runtime) *Object { + return nil +} + +func (p *valueProperty) ToNumber() Value { + return nil +} + +func (p *valueProperty) isWritable() bool { + return p.writable || p.setterFunc != nil +} + +func (p *valueProperty) get(this Value) Value { + if p.getterFunc == nil { + if p.value != nil { + return p.value + } + return _undefined + } + call, _ := p.getterFunc.self.assertCallable() + return call(FunctionCall{ + This: this, + }) +} + +func (p *valueProperty) set(this, v Value) { + if p.setterFunc == nil { + p.value = v + return + } + call, _ := p.setterFunc.self.assertCallable() + call(FunctionCall{ + This: this, + Arguments: []Value{v}, + }) +} + +func (p *valueProperty) SameAs(other Value) bool { + if otherProp, ok := other.(*valueProperty); ok { + return p == otherProp + } + return false +} + +func (p *valueProperty) Equals(Value) bool { + return false +} + +func (p *valueProperty) StrictEquals(Value) bool { + return false +} + +func (p *valueProperty) baseObject(r *Runtime) *Object { + r.typeErrorResult(true, "BUG: baseObject() is called on valueProperty") // TODO error message + return nil +} + +func (p *valueProperty) Export() interface{} { + panic("Cannot export valueProperty") +} + +func (p *valueProperty) ExportType() reflect.Type { + panic("Cannot export valueProperty") +} + +func (p *valueProperty) hash(*maphash.Hash) uint64 { + panic("valueProperty should never be used in maps or sets") +} + +func floatToIntClip(n float64) int64 { + switch { + case math.IsNaN(n): + return 0 + case n >= math.MaxInt64: + return math.MaxInt64 + case n <= math.MinInt64: + return math.MinInt64 + } + return int64(n) +} + +func (f valueFloat) ToInteger() int64 { + return floatToIntClip(float64(f)) +} + +func (f valueFloat) toString() String { + return asciiString(f.String()) +} + +func (f valueFloat) string() unistring.String { + return unistring.String(f.String()) +} + +func (f valueFloat) ToString() Value { + return f +} + +func (f valueFloat) String() string { + return fToStr(float64(f), ftoa.ModeStandard, 0) +} + +func (f valueFloat) ToFloat() float64 { + return float64(f) +} + +func (f valueFloat) ToBoolean() bool { + return float64(f) != 0.0 && !math.IsNaN(float64(f)) +} + +func (f valueFloat) ToObject(r *Runtime) *Object { + return r.newPrimitiveObject(f, r.getNumberPrototype(), "Number") +} + +func (f valueFloat) ToNumber() Value { + return f +} + +func (f valueFloat) SameAs(other Value) bool { + switch o := other.(type) { + case valueFloat: + this := float64(f) + o1 := float64(o) + if math.IsNaN(this) && math.IsNaN(o1) { + return true + } else { + ret := this == o1 + if ret && this == 0 { + ret = math.Signbit(this) == math.Signbit(o1) + } + return ret + } + case valueInt: + this := float64(f) + ret := this == float64(o) + if ret && this == 0 { + ret = !math.Signbit(this) + } + return ret + } + + return false +} + +func (f valueFloat) Equals(other Value) bool { + switch o := other.(type) { + case valueFloat: + return f == o + case valueInt: + return float64(f) == float64(o) + case *valueBigInt: + if IsInfinity(f) || math.IsNaN(float64(f)) { + return false + } + if f := big.NewFloat(float64(f)); f.IsInt() { + i, _ := f.Int(nil) + return (*big.Int)(o).Cmp(i) == 0 + } + return false + case String, valueBool: + return float64(f) == o.ToFloat() + case *Object: + return f.Equals(o.toPrimitive()) + } + + return false +} + +func (f valueFloat) StrictEquals(other Value) bool { + switch o := other.(type) { + case valueFloat: + return f == o + case valueInt: + return float64(f) == float64(o) + } + + return false +} + +func (f valueFloat) baseObject(r *Runtime) *Object { + return r.getNumberPrototype() +} + +func (f valueFloat) Export() interface{} { + return float64(f) +} + +func (f valueFloat) ExportType() reflect.Type { + return reflectTypeFloat +} + +func (f valueFloat) hash(*maphash.Hash) uint64 { + if f == _negativeZero { + return 0 + } + return math.Float64bits(float64(f)) +} + +func (o *Object) ToInteger() int64 { + return o.toPrimitiveNumber().ToNumber().ToInteger() +} + +func (o *Object) toString() String { + return o.toPrimitiveString().toString() +} + +func (o *Object) string() unistring.String { + return o.toPrimitiveString().string() +} + +func (o *Object) ToString() Value { + return o.toPrimitiveString().ToString() +} + +func (o *Object) String() string { + return o.toPrimitiveString().String() +} + +func (o *Object) ToFloat() float64 { + return o.toPrimitiveNumber().ToFloat() +} + +func (o *Object) ToBoolean() bool { + return true +} + +func (o *Object) ToObject(*Runtime) *Object { + return o +} + +func (o *Object) ToNumber() Value { + return o.toPrimitiveNumber().ToNumber() +} + +func (o *Object) SameAs(other Value) bool { + return o.StrictEquals(other) +} + +func (o *Object) Equals(other Value) bool { + if other, ok := other.(*Object); ok { + return o == other || o.self.equal(other.self) + } + + switch o1 := other.(type) { + case valueInt, valueFloat, *valueBigInt, String, *Symbol: + return o.toPrimitive().Equals(other) + case valueBool: + return o.Equals(o1.ToNumber()) + } + + return false +} + +func (o *Object) StrictEquals(other Value) bool { + if other, ok := other.(*Object); ok { + return o == other || o != nil && other != nil && o.self.equal(other.self) + } + return false +} + +func (o *Object) baseObject(*Runtime) *Object { + return o +} + +// Export the Object to a plain Go type. +// If the Object is a wrapped Go value (created using ToValue()) returns the original value. +// +// If the Object is a function, returns func(FunctionCall) Value. Note that exceptions thrown inside the function +// result in panics, which can also leave the Runtime in an unusable state. Therefore, these values should only +// be used inside another ES function implemented in Go. For calling a function from Go, use AssertFunction() or +// Runtime.ExportTo() as described in the README. +// +// For a Map, returns the list of entries as [][2]interface{}. +// +// For a Set, returns the list of elements as []interface{}. +// +// For a Proxy, returns Proxy. +// +// For a Promise, returns Promise. +// +// For a DynamicObject or a DynamicArray, returns the underlying handler. +// +// For typed arrays it returns a slice of the corresponding type backed by the original data (i.e. it does not copy). +// +// For an untyped array, returns its items exported into a newly created []interface{}. +// +// In all other cases returns own enumerable non-symbol properties as map[string]interface{}. +// +// This method will panic with an *Exception if a JavaScript exception is thrown in the process. Use Runtime.Try to catch these. +func (o *Object) Export() interface{} { + return o.self.export(&objectExportCtx{}) +} + +// ExportType returns the type of the value that is returned by Export(). +func (o *Object) ExportType() reflect.Type { + return o.self.exportType() +} + +func (o *Object) hash(*maphash.Hash) uint64 { + return o.getId() +} + +// Get an object's property by name. +// This method will panic with an *Exception if a JavaScript exception is thrown in the process. Use Runtime.Try to catch these. +func (o *Object) Get(name string) Value { + return o.self.getStr(unistring.NewFromString(name), nil) +} + +// GetSymbol returns the value of a symbol property. Use one of the Sym* values for well-known +// symbols (such as SymIterator, SymToStringTag, etc...). +// This method will panic with an *Exception if a JavaScript exception is thrown in the process. Use Runtime.Try to catch these. +func (o *Object) GetSymbol(sym *Symbol) Value { + return o.self.getSym(sym, nil) +} + +// Keys returns a list of Object's enumerable keys. +// This method will panic with an *Exception if a JavaScript exception is thrown in the process. Use Runtime.Try to catch these. +func (o *Object) Keys() (keys []string) { + iter := &enumerableIter{ + o: o, + wrapped: o.self.iterateStringKeys(), + } + for item, next := iter.next(); next != nil; item, next = next() { + keys = append(keys, item.name.String()) + } + + return +} + +// GetOwnPropertyNames returns a list of all own string properties of the Object, similar to Object.getOwnPropertyNames() +// This method will panic with an *Exception if a JavaScript exception is thrown in the process. Use Runtime.Try to catch these. +func (o *Object) GetOwnPropertyNames() (keys []string) { + for item, next := o.self.iterateStringKeys()(); next != nil; item, next = next() { + keys = append(keys, item.name.String()) + } + + return +} + +// Symbols returns a list of Object's enumerable symbol properties. +// This method will panic with an *Exception if a JavaScript exception is thrown in the process. Use Runtime.Try to catch these. +func (o *Object) Symbols() []*Symbol { + symbols := o.self.symbols(false, nil) + ret := make([]*Symbol, len(symbols)) + for i, sym := range symbols { + ret[i], _ = sym.(*Symbol) + } + return ret +} + +// DefineDataProperty is a Go equivalent of Object.defineProperty(o, name, {value: value, writable: writable, +// configurable: configurable, enumerable: enumerable}) +func (o *Object) DefineDataProperty(name string, value Value, writable, configurable, enumerable Flag) error { + return o.runtime.try(func() { + o.self.defineOwnPropertyStr(unistring.NewFromString(name), PropertyDescriptor{ + Value: value, + Writable: writable, + Configurable: configurable, + Enumerable: enumerable, + }, true) + }) +} + +// DefineAccessorProperty is a Go equivalent of Object.defineProperty(o, name, {get: getter, set: setter, +// configurable: configurable, enumerable: enumerable}) +func (o *Object) DefineAccessorProperty(name string, getter, setter Value, configurable, enumerable Flag) error { + return o.runtime.try(func() { + o.self.defineOwnPropertyStr(unistring.NewFromString(name), PropertyDescriptor{ + Getter: getter, + Setter: setter, + Configurable: configurable, + Enumerable: enumerable, + }, true) + }) +} + +// DefineDataPropertySymbol is a Go equivalent of Object.defineProperty(o, name, {value: value, writable: writable, +// configurable: configurable, enumerable: enumerable}) +func (o *Object) DefineDataPropertySymbol(name *Symbol, value Value, writable, configurable, enumerable Flag) error { + return o.runtime.try(func() { + o.self.defineOwnPropertySym(name, PropertyDescriptor{ + Value: value, + Writable: writable, + Configurable: configurable, + Enumerable: enumerable, + }, true) + }) +} + +// DefineAccessorPropertySymbol is a Go equivalent of Object.defineProperty(o, name, {get: getter, set: setter, +// configurable: configurable, enumerable: enumerable}) +func (o *Object) DefineAccessorPropertySymbol(name *Symbol, getter, setter Value, configurable, enumerable Flag) error { + return o.runtime.try(func() { + o.self.defineOwnPropertySym(name, PropertyDescriptor{ + Getter: getter, + Setter: setter, + Configurable: configurable, + Enumerable: enumerable, + }, true) + }) +} + +func (o *Object) Set(name string, value interface{}) error { + return o.runtime.try(func() { + o.self.setOwnStr(unistring.NewFromString(name), o.runtime.ToValue(value), true) + }) +} + +func (o *Object) SetSymbol(name *Symbol, value interface{}) error { + return o.runtime.try(func() { + o.self.setOwnSym(name, o.runtime.ToValue(value), true) + }) +} + +func (o *Object) Delete(name string) error { + return o.runtime.try(func() { + o.self.deleteStr(unistring.NewFromString(name), true) + }) +} + +func (o *Object) DeleteSymbol(name *Symbol) error { + return o.runtime.try(func() { + o.self.deleteSym(name, true) + }) +} + +// Prototype returns the Object's prototype, same as Object.getPrototypeOf(). If the prototype is null +// returns nil. +func (o *Object) Prototype() *Object { + return o.self.proto() +} + +// SetPrototype sets the Object's prototype, same as Object.setPrototypeOf(). Setting proto to nil +// is an equivalent of Object.setPrototypeOf(null). +func (o *Object) SetPrototype(proto *Object) error { + return o.runtime.try(func() { + o.self.setProto(proto, true) + }) +} + +// MarshalJSON returns JSON representation of the Object. It is equivalent to JSON.stringify(o). +// Note, this implements json.Marshaler so that json.Marshal() can be used without the need to Export(). +func (o *Object) MarshalJSON() ([]byte, error) { + ctx := _builtinJSON_stringifyContext{ + r: o.runtime, + } + ex := o.runtime.vm.try(func() { + if !ctx.do(o) { + ctx.buf.WriteString("null") + } + }) + if ex != nil { + return nil, ex + } + return ctx.buf.Bytes(), nil +} + +// UnmarshalJSON implements the json.Unmarshaler interface. It is added to compliment MarshalJSON, because +// some alternative JSON encoders refuse to use MarshalJSON unless UnmarshalJSON is also present. +// It is a no-op and always returns nil. +func (o *Object) UnmarshalJSON([]byte) error { + return nil +} + +// ClassName returns the class name +func (o *Object) ClassName() string { + return o.self.className() +} + +func (o valueUnresolved) throw() { + o.r.throwReferenceError(o.ref) +} + +func (o valueUnresolved) ToInteger() int64 { + o.throw() + return 0 +} + +func (o valueUnresolved) toString() String { + o.throw() + return nil +} + +func (o valueUnresolved) string() unistring.String { + o.throw() + return "" +} + +func (o valueUnresolved) ToString() Value { + o.throw() + return nil +} + +func (o valueUnresolved) String() string { + o.throw() + return "" +} + +func (o valueUnresolved) ToFloat() float64 { + o.throw() + return 0 +} + +func (o valueUnresolved) ToBoolean() bool { + o.throw() + return false +} + +func (o valueUnresolved) ToObject(*Runtime) *Object { + o.throw() + return nil +} + +func (o valueUnresolved) ToNumber() Value { + o.throw() + return nil +} + +func (o valueUnresolved) SameAs(Value) bool { + o.throw() + return false +} + +func (o valueUnresolved) Equals(Value) bool { + o.throw() + return false +} + +func (o valueUnresolved) StrictEquals(Value) bool { + o.throw() + return false +} + +func (o valueUnresolved) baseObject(*Runtime) *Object { + o.throw() + return nil +} + +func (o valueUnresolved) Export() interface{} { + o.throw() + return nil +} + +func (o valueUnresolved) ExportType() reflect.Type { + o.throw() + return nil +} + +func (o valueUnresolved) hash(*maphash.Hash) uint64 { + o.throw() + return 0 +} + +func (s *Symbol) ToInteger() int64 { + panic(typeError("Cannot convert a Symbol value to a number")) +} + +func (s *Symbol) toString() String { + panic(typeError("Cannot convert a Symbol value to a string")) +} + +func (s *Symbol) ToString() Value { + return s +} + +func (s *Symbol) String() string { + if s.desc != nil { + return s.desc.String() + } + return "" +} + +func (s *Symbol) string() unistring.String { + if s.desc != nil { + return s.desc.string() + } + return "" +} + +func (s *Symbol) ToFloat() float64 { + panic(typeError("Cannot convert a Symbol value to a number")) +} + +func (s *Symbol) ToNumber() Value { + panic(typeError("Cannot convert a Symbol value to a number")) +} + +func (s *Symbol) ToBoolean() bool { + return true +} + +func (s *Symbol) ToObject(r *Runtime) *Object { + return s.baseObject(r) +} + +func (s *Symbol) SameAs(other Value) bool { + if s1, ok := other.(*Symbol); ok { + return s == s1 + } + return false +} + +func (s *Symbol) Equals(o Value) bool { + switch o := o.(type) { + case *Object: + return s.Equals(o.toPrimitive()) + } + return s.SameAs(o) +} + +func (s *Symbol) StrictEquals(o Value) bool { + return s.SameAs(o) +} + +func (s *Symbol) Export() interface{} { + return s.String() +} + +func (s *Symbol) ExportType() reflect.Type { + return reflectTypeString +} + +func (s *Symbol) baseObject(r *Runtime) *Object { + return r.newPrimitiveObject(s, r.getSymbolPrototype(), classObject) +} + +func (s *Symbol) hash(*maphash.Hash) uint64 { + return uint64(s.h) +} + +func exportValue(v Value, ctx *objectExportCtx) interface{} { + if obj, ok := v.(*Object); ok { + return obj.self.export(ctx) + } + return v.Export() +} + +func newSymbol(s String) *Symbol { + r := &Symbol{ + desc: s, + } + // This may need to be reconsidered in the future. + // Depending on changes in Go's allocation policy and/or introduction of a compacting GC + // this may no longer provide sufficient dispersion. The alternative, however, is a globally + // synchronised random generator/hasher/sequencer and I don't want to go down that route just yet. + r.h = uintptr(unsafe.Pointer(r)) + return r +} + +func NewSymbol(s string) *Symbol { + return newSymbol(newStringValue(s)) +} + +func (s *Symbol) descriptiveString() String { + desc := s.desc + if desc == nil { + desc = stringEmpty + } + return asciiString("Symbol(").Concat(desc).Concat(asciiString(")")) +} + +func funcName(prefix string, n Value) String { + var b StringBuilder + b.WriteString(asciiString(prefix)) + if sym, ok := n.(*Symbol); ok { + if sym.desc != nil { + b.WriteRune('[') + b.WriteString(sym.desc) + b.WriteRune(']') + } + } else { + b.WriteString(n.toString()) + } + return b.String() +} + +func newTypeError(args ...interface{}) typeError { + msg := "" + if len(args) > 0 { + f, _ := args[0].(string) + msg = fmt.Sprintf(f, args[1:]...) + } + return typeError(msg) +} + +func typeErrorResult(throw bool, args ...interface{}) { + if throw { + panic(newTypeError(args...)) + } + +} + +func init() { + for i := 0; i < 256; i++ { + intCache[i] = valueInt(i - 256) + } +} diff --git a/goja/vm.go b/goja/vm.go new file mode 100644 index 0000000..35e5594 --- /dev/null +++ b/goja/vm.go @@ -0,0 +1,5917 @@ +package goja + +import ( + "fmt" + "math" + "math/big" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/dop251/goja/unistring" +) + +const ( + maxInt = 1 << 53 + + tryPanicMarker = -2 +) + +type valueStack []Value + +type stash struct { + values []Value + extraArgs []Value + names map[unistring.String]uint32 + obj *Object + + outer *stash + + // If this is a top-level function stash, sets the type of the function. If set, dynamic var declarations + // created by direct eval go here. + funcType funcType +} + +type context struct { + prg *Program + stash *stash + privEnv *privateEnv + newTarget Value + result Value + pc, sb int + args int +} + +type tryFrame struct { + // holds an uncaught exception for the 'finally' block + exception *Exception + + callStackLen, iterLen, refLen uint32 + + sp int32 + stash *stash + privEnv *privateEnv + + catchPos, finallyPos, finallyRet int32 +} + +type execCtx struct { + context + stack []Value + tryStack []tryFrame + iterStack []iterStackItem + refStack []ref +} + +func (vm *vm) suspend(ectx *execCtx, tryStackLen, iterStackLen, refStackLen uint32) { + vm.saveCtx(&ectx.context) + ectx.stack = append(ectx.stack[:0], vm.stack[vm.sb-1:vm.sp]...) + if len(vm.tryStack) > int(tryStackLen) { + ectx.tryStack = append(ectx.tryStack[:0], vm.tryStack[tryStackLen:]...) + vm.tryStack = vm.tryStack[:tryStackLen] + sp := int32(vm.sb - 1) + for i := range ectx.tryStack { + tf := &ectx.tryStack[i] + tf.iterLen -= iterStackLen + tf.refLen -= refStackLen + tf.sp -= sp + } + } + if len(vm.iterStack) > int(iterStackLen) { + ectx.iterStack = append(ectx.iterStack[:0], vm.iterStack[iterStackLen:]...) + vm.iterStack = vm.iterStack[:iterStackLen] + } + if len(vm.refStack) > int(refStackLen) { + ectx.refStack = append(ectx.refStack[:0], vm.refStack[refStackLen:]...) + vm.refStack = vm.refStack[:refStackLen] + } +} + +func (vm *vm) resume(ctx *execCtx) { + vm.restoreCtx(&ctx.context) + sp := vm.sp + vm.sb = sp + 1 + vm.stack.expand(sp + len(ctx.stack)) + copy(vm.stack[sp:], ctx.stack) + vm.sp += len(ctx.stack) + for i := range ctx.tryStack { + tf := &ctx.tryStack[i] + tf.callStackLen = uint32(len(vm.callStack)) + tf.iterLen += uint32(len(vm.iterStack)) + tf.refLen += uint32(len(vm.refStack)) + tf.sp += int32(sp) + } + vm.tryStack = append(vm.tryStack, ctx.tryStack...) + vm.iterStack = append(vm.iterStack, ctx.iterStack...) + vm.refStack = append(vm.refStack, ctx.refStack...) +} + +type iterStackItem struct { + val Value + f iterNextFunc + iter *iteratorRecord +} + +type ref interface { + get() Value + set(Value) + init(Value) + refname() unistring.String +} + +type stashRef struct { + n unistring.String + v *[]Value + idx int +} + +func (r *stashRef) get() Value { + return nilSafe((*r.v)[r.idx]) +} + +func (r *stashRef) set(v Value) { + (*r.v)[r.idx] = v +} + +func (r *stashRef) init(v Value) { + r.set(v) +} + +func (r *stashRef) refname() unistring.String { + return r.n +} + +type thisRef struct { + v *[]Value + idx int +} + +func (r *thisRef) get() Value { + v := (*r.v)[r.idx] + if v == nil { + panic(referenceError("Must call super constructor in derived class before accessing 'this'")) + } + + return v +} + +func (r *thisRef) set(v Value) { + ptr := &(*r.v)[r.idx] + if *ptr != nil { + panic(referenceError("Super constructor may only be called once")) + } + *ptr = v +} + +func (r *thisRef) init(v Value) { + r.set(v) +} + +func (r *thisRef) refname() unistring.String { + return thisBindingName +} + +type stashRefLex struct { + stashRef +} + +func (r *stashRefLex) get() Value { + v := (*r.v)[r.idx] + if v == nil { + panic(errAccessBeforeInit) + } + return v +} + +func (r *stashRefLex) set(v Value) { + p := &(*r.v)[r.idx] + if *p == nil { + panic(errAccessBeforeInit) + } + *p = v +} + +func (r *stashRefLex) init(v Value) { + (*r.v)[r.idx] = v +} + +type stashRefConst struct { + stashRefLex + strictConst bool +} + +func (r *stashRefConst) set(v Value) { + if r.strictConst { + panic(errAssignToConst) + } +} + +type objRef struct { + base *Object + name Value + this Value + strict bool + + nameConverted bool +} + +func (r *objRef) getKey() Value { + if !r.nameConverted { + r.name = toPropertyKey(r.name) + r.nameConverted = true + } + return r.name +} + +func (r *objRef) get() Value { + return r.base.get(r.getKey(), r.this) +} + +func (r *objRef) set(v Value) { + key := r.getKey() + if r.this != nil { + r.base.set(key, v, r.this, r.strict) + } else { + r.base.setOwn(key, v, r.strict) + } +} + +func (r *objRef) init(v Value) { + if r.this != nil { + r.base.set(r.getKey(), v, r.this, r.strict) + } else { + r.base.setOwn(r.getKey(), v, r.strict) + } +} + +func (r *objRef) refname() unistring.String { + return r.getKey().string() +} + +type objStrRef struct { + base *Object + name unistring.String + this Value + strict bool + binding bool +} + +func (r *objStrRef) get() Value { + return r.base.self.getStr(r.name, r.this) +} + +func (r *objStrRef) set(v Value) { + if r.strict && r.binding && !r.base.self.hasOwnPropertyStr(r.name) { + panic(referenceError(fmt.Sprintf("%s is not defined", r.name))) + } + if r.this != nil { + r.base.setStr(r.name, v, r.this, r.strict) + } else { + r.base.self.setOwnStr(r.name, v, r.strict) + } +} + +func (r *objStrRef) init(v Value) { + if r.this != nil { + r.base.setStr(r.name, v, r.this, r.strict) + } else { + r.base.self.setOwnStr(r.name, v, r.strict) + } +} + +func (r *objStrRef) refname() unistring.String { + return r.name +} + +type privateRefRes struct { + base *Object + name *resolvedPrivateName +} + +func (p *privateRefRes) get() Value { + return (*getPrivatePropRes)(p.name)._get(p.base, p.base.runtime.vm) +} + +func (p *privateRefRes) set(value Value) { + (*setPrivatePropRes)(p.name)._set(p.base, value, p.base.runtime.vm) +} + +func (p *privateRefRes) init(value Value) { + panic("not supported") +} + +func (p *privateRefRes) refname() unistring.String { + return p.name.string() +} + +type privateRefId struct { + base *Object + id *privateId +} + +func (p *privateRefId) get() Value { + return p.base.runtime.vm.getPrivateProp(p.base, p.id.name, p.id.typ, p.id.idx, p.id.isMethod) +} + +func (p *privateRefId) set(value Value) { + p.base.runtime.vm.setPrivateProp(p.base, p.id.name, p.id.typ, p.id.idx, p.id.isMethod, value) +} + +func (p *privateRefId) init(value Value) { + panic("not supported") +} + +func (p *privateRefId) refname() unistring.String { + return p.id.string() +} + +type unresolvedRef struct { + runtime *Runtime + name unistring.String +} + +func (r *unresolvedRef) get() Value { + r.runtime.throwReferenceError(r.name) + panic("Unreachable") +} + +func (r *unresolvedRef) set(Value) { + r.get() +} + +func (r *unresolvedRef) init(Value) { + r.get() +} + +func (r *unresolvedRef) refname() unistring.String { + return r.name +} + +type vm struct { + r *Runtime + prg *Program + pc int + stack valueStack + sp, sb, args int + + stash *stash + privEnv *privateEnv + callStack []context + iterStack []iterStackItem + refStack []ref + tryStack []tryFrame + newTarget Value + result Value + + maxCallStackSize int + + stashAllocs int + + interrupted uint32 + interruptVal interface{} + interruptLock sync.Mutex + + curAsyncRunner *asyncRunner + + profTracker *profTracker +} + +type instruction interface { + exec(*vm) +} + +func intToValue(i int64) Value { + if idx := 256 + i; idx >= 0 && idx < 256 { + return intCache[idx] + } + if i >= -maxInt && i <= maxInt { + return valueInt(i) + } + return valueFloat(i) +} + +func floatToInt(f float64) (result int64, ok bool) { + if (f != 0 || !math.Signbit(f)) && !math.IsInf(f, 0) && f == math.Trunc(f) && f >= -maxInt && f <= maxInt { + return int64(f), true + } + return 0, false +} + +func floatToValue(f float64) (result Value) { + if i, ok := floatToInt(f); ok { + return intToValue(i) + } + switch { + case f == 0: + return _negativeZero + case math.IsNaN(f): + return _NaN + case math.IsInf(f, 1): + return _positiveInf + case math.IsInf(f, -1): + return _negativeInf + } + return valueFloat(f) +} + +func toNumeric(value Value) Value { + switch v := value.(type) { + case valueInt, *valueBigInt: + return v + case valueFloat: + return floatToValue(float64(v)) + case *Object: + primValue := v.toPrimitiveNumber() + if bigint, ok := primValue.(*valueBigInt); ok { + return bigint + } + return primValue.ToNumber() + } + return value.ToNumber() +} + +func (s *valueStack) expand(idx int) { + if idx < len(*s) { + return + } + idx++ + if idx < cap(*s) { + *s = (*s)[:idx] + } else { + var newCap int + if idx < 1024 { + newCap = idx * 2 + } else { + newCap = (idx + 1025) &^ 1023 + } + n := make([]Value, idx, newCap) + copy(n, *s) + *s = n + } +} + +func stashObjHas(obj *Object, name unistring.String) bool { + if obj.self.hasPropertyStr(name) { + if unscopables, ok := obj.self.getSym(SymUnscopables, nil).(*Object); ok { + if b := unscopables.self.getStr(name, nil); b != nil { + return !b.ToBoolean() + } + } + return true + } + return false +} + +func (s *stash) isVariable() bool { + return s.funcType != funcNone +} + +func (s *stash) initByIdx(idx uint32, v Value) { + if s.obj != nil { + panic("Attempt to init by idx into an object scope") + } + s.values[idx] = v +} + +func (s *stash) initByName(name unistring.String, v Value) { + if idx, exists := s.names[name]; exists { + s.values[idx&^maskTyp] = v + } else { + panic(referenceError(fmt.Sprintf("%s is not defined", name))) + } +} + +func (s *stash) getByIdx(idx uint32) Value { + return s.values[idx] +} + +func (s *stash) getByName(name unistring.String) (v Value, exists bool) { + if s.obj != nil { + if stashObjHas(s.obj, name) { + return nilSafe(s.obj.self.getStr(name, nil)), true + } + return nil, false + } + if idx, exists := s.names[name]; exists { + v := s.values[idx&^maskTyp] + if v == nil { + if idx&maskVar == 0 { + panic(errAccessBeforeInit) + } else { + v = _undefined + } + } + return v, true + } + return nil, false +} + +func (s *stash) getRefByName(name unistring.String, strict bool) ref { + if obj := s.obj; obj != nil { + if stashObjHas(obj, name) { + return &objStrRef{ + base: obj, + name: name, + strict: strict, + binding: true, + } + } + } else { + if idx, exists := s.names[name]; exists { + if idx&maskVar == 0 { + if idx&maskConst == 0 { + return &stashRefLex{ + stashRef: stashRef{ + n: name, + v: &s.values, + idx: int(idx &^ maskTyp), + }, + } + } else { + return &stashRefConst{ + stashRefLex: stashRefLex{ + stashRef: stashRef{ + n: name, + v: &s.values, + idx: int(idx &^ maskTyp), + }, + }, + strictConst: strict || (idx&maskStrict != 0), + } + } + } else { + return &stashRef{ + n: name, + v: &s.values, + idx: int(idx &^ maskTyp), + } + } + } + } + return nil +} + +func (s *stash) createBinding(name unistring.String, deletable bool) { + if s.names == nil { + s.names = make(map[unistring.String]uint32) + } + if _, exists := s.names[name]; !exists { + idx := uint32(len(s.names)) | maskVar + if deletable { + idx |= maskDeletable + } + s.names[name] = idx + s.values = append(s.values, _undefined) + } +} + +func (s *stash) createLexBinding(name unistring.String, isConst bool) { + if s.names == nil { + s.names = make(map[unistring.String]uint32) + } + if _, exists := s.names[name]; !exists { + idx := uint32(len(s.names)) + if isConst { + idx |= maskConst | maskStrict + } + s.names[name] = idx + s.values = append(s.values, nil) + } +} + +func (s *stash) deleteBinding(name unistring.String) { + delete(s.names, name) +} + +func (vm *vm) newStash() { + vm.stash = &stash{ + outer: vm.stash, + } + vm.stashAllocs++ +} + +func (vm *vm) init() { + vm.sb = -1 + vm.stash = &vm.r.global.stash + vm.maxCallStackSize = math.MaxInt32 +} + +func (vm *vm) halted() bool { + pc := vm.pc + return pc < 0 || pc >= len(vm.prg.code) +} + +func (vm *vm) run() { + if vm.profTracker != nil && !vm.runWithProfiler() { + return + } + count := 0 + interrupted := false + for { + if count == 0 { + if atomic.LoadInt32(&globalProfiler.enabled) == 1 && !vm.runWithProfiler() { + return + } + count = 100 + } else { + count-- + } + if interrupted = atomic.LoadUint32(&vm.interrupted) != 0; interrupted { + break + } + pc := vm.pc + if pc < 0 || pc >= len(vm.prg.code) { + break + } + vm.prg.code[pc].exec(vm) + } + + if interrupted { + vm.interruptLock.Lock() + v := &InterruptedError{ + iface: vm.interruptVal, + } + v.stack = vm.captureStack(nil, 0) + vm.interruptLock.Unlock() + panic(v) + } +} + +func (vm *vm) runWithProfiler() bool { + pt := vm.profTracker + if pt == nil { + pt = globalProfiler.p.registerVm() + vm.profTracker = pt + defer func() { + atomic.StoreInt32(&vm.profTracker.finished, 1) + vm.profTracker = nil + }() + } + interrupted := false + for { + if interrupted = atomic.LoadUint32(&vm.interrupted) != 0; interrupted { + return true + } + pc := vm.pc + if pc < 0 || pc >= len(vm.prg.code) { + break + } + vm.prg.code[pc].exec(vm) + req := atomic.LoadInt32(&pt.req) + if req == profReqStop { + return true + } + if req == profReqDoSample { + pt.stop = time.Now() + + pt.numFrames = len(vm.r.CaptureCallStack(len(pt.frames), pt.frames[:0])) + pt.frames[0].pc = pc + atomic.StoreInt32(&pt.req, profReqSampleReady) + } + } + + return false +} + +func (vm *vm) Interrupt(v interface{}) { + vm.interruptLock.Lock() + vm.interruptVal = v + atomic.StoreUint32(&vm.interrupted, 1) + vm.interruptLock.Unlock() +} + +func (vm *vm) ClearInterrupt() { + atomic.StoreUint32(&vm.interrupted, 0) +} + +func getFuncName(stack []Value, sb int) unistring.String { + if sb > 0 { + if f, ok := stack[sb-1].(*Object); ok { + if _, isProxy := f.self.(*proxyObject); isProxy { + return "proxy" + } + return nilSafe(f.self.getStr("name", nil)).string() + } + } + return "" +} + +func (vm *vm) captureStack(stack []StackFrame, ctxOffset int) []StackFrame { + // Unroll the context stack + if vm.prg != nil || vm.sb > 0 { + var funcName unistring.String + if vm.prg != nil { + funcName = vm.prg.funcName + } else { + funcName = getFuncName(vm.stack, vm.sb) + } + stack = append(stack, StackFrame{prg: vm.prg, pc: vm.pc, funcName: funcName}) + } + for i := len(vm.callStack) - 1; i > ctxOffset-1; i-- { + frame := &vm.callStack[i] + if frame.prg != nil || frame.sb > 0 { + var funcName unistring.String + if prg := frame.prg; prg != nil { + funcName = prg.funcName + } else { + funcName = getFuncName(vm.stack, frame.sb) + } + stack = append(stack, StackFrame{prg: vm.callStack[i].prg, pc: frame.pc, funcName: funcName}) + } + } + if ctxOffset == 0 && vm.curAsyncRunner != nil { + stack = vm.captureAsyncStack(stack, vm.curAsyncRunner) + } + return stack +} + +func (vm *vm) captureAsyncStack(stack []StackFrame, runner *asyncRunner) []StackFrame { + if promise, _ := runner.promiseCap.promise.self.(*Promise); promise != nil { + if len(promise.fulfillReactions) == 1 { + if r := promise.fulfillReactions[0].asyncRunner; r != nil { + ctx := &r.gen.ctx + if ctx.prg != nil || ctx.sb > 0 { + var funcName unistring.String + if prg := ctx.prg; prg != nil { + funcName = prg.funcName + } else { + funcName = getFuncName(ctx.stack, 1) + } + stack = append(stack, StackFrame{prg: ctx.prg, pc: ctx.pc, funcName: funcName}) + } + stack = vm.captureAsyncStack(stack, r) + } + } + } + + return stack +} + +func (vm *vm) pushTryFrame(catchPos, finallyPos int32) { + vm.tryStack = append(vm.tryStack, tryFrame{ + callStackLen: uint32(len(vm.callStack)), + iterLen: uint32(len(vm.iterStack)), + refLen: uint32(len(vm.refStack)), + sp: int32(vm.sp), + stash: vm.stash, + privEnv: vm.privEnv, + catchPos: catchPos, + finallyPos: finallyPos, + finallyRet: -1, + }) +} + +func (vm *vm) popTryFrame() { + vm.tryStack = vm.tryStack[:len(vm.tryStack)-1] +} + +func (vm *vm) restoreStacks(iterLen, refLen uint32) (ex *Exception) { + // Restore other stacks + iterTail := vm.iterStack[iterLen:] + for i := len(iterTail) - 1; i >= 0; i-- { + if iter := iterTail[i].iter; iter != nil { + ex1 := vm.try(func() { + iter.returnIter() + }) + if ex1 != nil && ex == nil { + ex = ex1 + } + } + iterTail[i] = iterStackItem{} + } + vm.iterStack = vm.iterStack[:iterLen] + refTail := vm.refStack[refLen:] + for i := range refTail { + refTail[i] = nil + } + vm.refStack = vm.refStack[:refLen] + return +} + +func (vm *vm) handleThrow(arg interface{}) *Exception { + ex := vm.exceptionFromValue(arg) + for len(vm.tryStack) > 0 { + tf := &vm.tryStack[len(vm.tryStack)-1] + if tf.catchPos == -1 && tf.finallyPos == -1 || ex == nil && tf.catchPos != tryPanicMarker { + tf.exception = nil + vm.popTryFrame() + continue + } + if int(tf.callStackLen) < len(vm.callStack) { + ctx := &vm.callStack[tf.callStackLen] + vm.prg, vm.newTarget, vm.result, vm.pc, vm.sb, vm.args = + ctx.prg, ctx.newTarget, ctx.result, ctx.pc, ctx.sb, ctx.args + vm.callStack = vm.callStack[:tf.callStackLen] + } + vm.sp = int(tf.sp) + vm.stash = tf.stash + vm.privEnv = tf.privEnv + _ = vm.restoreStacks(tf.iterLen, tf.refLen) + + if tf.catchPos == tryPanicMarker { + break + } + + if tf.catchPos >= 0 { + // exception is caught + vm.push(ex.val) + vm.pc = int(tf.catchPos) + tf.catchPos = -1 + return nil + } + if tf.finallyPos >= 0 { + // no 'catch' block, but there is a 'finally' block + tf.exception = ex + vm.pc = int(tf.finallyPos) + tf.finallyPos = -1 + tf.finallyRet = -1 + return nil + } + } + if ex == nil { + panic(arg) + } + return ex +} + +// Calls to this method must be made from the run() loop and must be the last statement before 'return'. +// In all other cases exceptions must be thrown using panic(). +func (vm *vm) throw(v interface{}) { + if ex := vm.handleThrow(v); ex != nil { + panic(ex) + } +} + +func (vm *vm) try(f func()) (ex *Exception) { + vm.pushTryFrame(tryPanicMarker, -1) + defer vm.popTryFrame() + + defer func() { + if x := recover(); x != nil { + ex = vm.handleThrow(x) + } + }() + + f() + return +} + +func (vm *vm) runTry() (ex *Exception) { + vm.pushTryFrame(tryPanicMarker, -1) + defer vm.popTryFrame() + + for { + ex = vm.runTryInner() + if ex != nil || vm.halted() { + return + } + } +} + +func (vm *vm) runTryInner() (ex *Exception) { + defer func() { + if x := recover(); x != nil { + ex = vm.handleThrow(x) + } + }() + + vm.run() + return +} + +func (vm *vm) push(v Value) { + vm.stack.expand(vm.sp) + vm.stack[vm.sp] = v + vm.sp++ +} + +func (vm *vm) pop() Value { + vm.sp-- + return vm.stack[vm.sp] +} + +func (vm *vm) peek() Value { + return vm.stack[vm.sp-1] +} + +func (vm *vm) saveCtx(ctx *context) { + ctx.prg, ctx.stash, ctx.privEnv, ctx.newTarget, ctx.result, ctx.pc, ctx.sb, ctx.args = + vm.prg, vm.stash, vm.privEnv, vm.newTarget, vm.result, vm.pc, vm.sb, vm.args +} + +func (vm *vm) pushCtx() { + if len(vm.callStack) > vm.maxCallStackSize { + ex := &StackOverflowError{} + ex.stack = vm.captureStack(nil, 0) + panic(ex) + } + vm.callStack = append(vm.callStack, context{}) + ctx := &vm.callStack[len(vm.callStack)-1] + vm.saveCtx(ctx) +} + +func (vm *vm) restoreCtx(ctx *context) { + vm.prg, vm.stash, vm.privEnv, vm.newTarget, vm.result, vm.pc, vm.sb, vm.args = + ctx.prg, ctx.stash, ctx.privEnv, ctx.newTarget, ctx.result, ctx.pc, ctx.sb, ctx.args +} + +func (vm *vm) popCtx() { + l := len(vm.callStack) - 1 + ctx := &vm.callStack[l] + vm.restoreCtx(ctx) + + if ctx.prg != nil { + *ctx = context{} + } + + vm.callStack = vm.callStack[:l] +} + +func (vm *vm) toCallee(v Value) *Object { + if obj, ok := v.(*Object); ok { + return obj + } + switch unresolved := v.(type) { + case valueUnresolved: + unresolved.throw() + panic("Unreachable") + case memberUnresolved: + panic(vm.r.NewTypeError("Object has no member '%s'", unresolved.ref)) + } + panic(vm.r.NewTypeError("Value is not an object: %s", v.toString())) +} + +type loadVal struct { + v Value +} + +func (l loadVal) exec(vm *vm) { + vm.push(l.v) + vm.pc++ +} + +type _loadUndef struct{} + +var loadUndef _loadUndef + +func (_loadUndef) exec(vm *vm) { + vm.push(_undefined) + vm.pc++ +} + +type _loadNil struct{} + +var loadNil _loadNil + +func (_loadNil) exec(vm *vm) { + vm.push(nil) + vm.pc++ +} + +type _saveResult struct{} + +var saveResult _saveResult + +func (_saveResult) exec(vm *vm) { + vm.sp-- + vm.result = vm.stack[vm.sp] + vm.pc++ +} + +type _loadResult struct{} + +var loadResult _loadResult + +func (_loadResult) exec(vm *vm) { + vm.push(vm.result) + vm.pc++ +} + +type _clearResult struct{} + +var clearResult _clearResult + +func (_clearResult) exec(vm *vm) { + vm.result = _undefined + vm.pc++ +} + +type _loadGlobalObject struct{} + +var loadGlobalObject _loadGlobalObject + +func (_loadGlobalObject) exec(vm *vm) { + vm.push(vm.r.globalObject) + vm.pc++ +} + +type loadStack int + +func (l loadStack) exec(vm *vm) { + // l > 0 -- var + // l == 0 -- this + + if l > 0 { + vm.push(nilSafe(vm.stack[vm.sb+vm.args+int(l)])) + } else { + vm.push(vm.stack[vm.sb]) + } + vm.pc++ +} + +type loadStack1 int + +func (l loadStack1) exec(vm *vm) { + // args are in stash + // l > 0 -- var + // l == 0 -- this + + if l > 0 { + vm.push(nilSafe(vm.stack[vm.sb+int(l)])) + } else { + vm.push(vm.stack[vm.sb]) + } + vm.pc++ +} + +type loadStackLex int + +func (l loadStackLex) exec(vm *vm) { + // l < 0 -- arg<-l-1> + // l > 0 -- var + // l == 0 -- this + var p *Value + if l <= 0 { + arg := int(-l) + if arg > vm.args { + vm.push(_undefined) + vm.pc++ + return + } else { + p = &vm.stack[vm.sb+arg] + } + } else { + p = &vm.stack[vm.sb+vm.args+int(l)] + } + if *p == nil { + vm.throw(errAccessBeforeInit) + return + } + vm.push(*p) + vm.pc++ +} + +type loadStack1Lex int + +func (l loadStack1Lex) exec(vm *vm) { + p := &vm.stack[vm.sb+int(l)] + if *p == nil { + vm.throw(errAccessBeforeInit) + return + } + vm.push(*p) + vm.pc++ +} + +type _loadCallee struct{} + +var loadCallee _loadCallee + +func (_loadCallee) exec(vm *vm) { + vm.push(vm.stack[vm.sb-1]) + vm.pc++ +} + +func (vm *vm) storeStack(s int) { + // l > 0 -- var + + if s > 0 { + vm.stack[vm.sb+vm.args+s] = vm.stack[vm.sp-1] + } else { + panic("Illegal stack var index") + } + vm.pc++ +} + +func (vm *vm) storeStack1(s int) { + // args are in stash + // l > 0 -- var + + if s > 0 { + vm.stack[vm.sb+s] = vm.stack[vm.sp-1] + } else { + panic("Illegal stack var index") + } + vm.pc++ +} + +func (vm *vm) storeStackLex(s int) { + // l < 0 -- arg<-l-1> + // l > 0 -- var + var p *Value + if s < 0 { + p = &vm.stack[vm.sb-s] + } else { + p = &vm.stack[vm.sb+vm.args+s] + } + + if *p != nil { + *p = vm.stack[vm.sp-1] + } else { + panic(errAccessBeforeInit) + } + vm.pc++ +} + +func (vm *vm) storeStack1Lex(s int) { + // args are in stash + // s > 0 -- var + if s <= 0 { + panic("Illegal stack var index") + } + p := &vm.stack[vm.sb+s] + if *p != nil { + *p = vm.stack[vm.sp-1] + } else { + panic(errAccessBeforeInit) + } + vm.pc++ +} + +func (vm *vm) initStack(s int) { + if s <= 0 { + vm.stack[vm.sb-s] = vm.stack[vm.sp-1] + } else { + vm.stack[vm.sb+vm.args+s] = vm.stack[vm.sp-1] + } + vm.pc++ +} + +func (vm *vm) initStack1(s int) { + if s <= 0 { + panic("Illegal stack var index") + } + vm.stack[vm.sb+s] = vm.stack[vm.sp-1] + vm.pc++ +} + +type storeStack int + +func (s storeStack) exec(vm *vm) { + vm.storeStack(int(s)) +} + +type storeStack1 int + +func (s storeStack1) exec(vm *vm) { + vm.storeStack1(int(s)) +} + +type storeStackLex int + +func (s storeStackLex) exec(vm *vm) { + vm.storeStackLex(int(s)) +} + +type storeStack1Lex int + +func (s storeStack1Lex) exec(vm *vm) { + vm.storeStack1Lex(int(s)) +} + +type initStack int + +func (s initStack) exec(vm *vm) { + vm.initStack(int(s)) +} + +type initStackP int + +func (s initStackP) exec(vm *vm) { + vm.initStack(int(s)) + vm.sp-- +} + +type initStack1 int + +func (s initStack1) exec(vm *vm) { + vm.initStack1(int(s)) +} + +type initStack1P int + +func (s initStack1P) exec(vm *vm) { + vm.initStack1(int(s)) + vm.sp-- +} + +type storeStackP int + +func (s storeStackP) exec(vm *vm) { + vm.storeStack(int(s)) + vm.sp-- +} + +type storeStack1P int + +func (s storeStack1P) exec(vm *vm) { + vm.storeStack1(int(s)) + vm.sp-- +} + +type storeStackLexP int + +func (s storeStackLexP) exec(vm *vm) { + vm.storeStackLex(int(s)) + vm.sp-- +} + +type storeStack1LexP int + +func (s storeStack1LexP) exec(vm *vm) { + vm.storeStack1Lex(int(s)) + vm.sp-- +} + +type _toNumber struct{} + +var toNumber _toNumber + +func (_toNumber) exec(vm *vm) { + vm.stack[vm.sp-1] = toNumeric(vm.stack[vm.sp-1]) + vm.pc++ +} + +type _add struct{} + +var add _add + +func (_add) exec(vm *vm) { + right := vm.stack[vm.sp-1] + left := vm.stack[vm.sp-2] + + if o, ok := left.(*Object); ok { + left = o.toPrimitive() + } + + if o, ok := right.(*Object); ok { + right = o.toPrimitive() + } + + var ret Value + + leftString, isLeftString := left.(String) + rightString, isRightString := right.(String) + + if isLeftString || isRightString { + if !isLeftString { + leftString = left.toString() + } + if !isRightString { + rightString = right.toString() + } + ret = leftString.Concat(rightString) + } else { + switch left := left.(type) { + case valueInt: + switch right := right.(type) { + case valueInt: + ret = intToValue(int64(left) + int64(right)) + case *valueBigInt: + panic(errMixBigIntType) + default: + ret = floatToValue(float64(left) + right.ToFloat()) + } + case *valueBigInt: + if right, ok := right.(*valueBigInt); ok { + ret = (*valueBigInt)(new(big.Int).Add((*big.Int)(left), (*big.Int)(right))) + } else { + panic(errMixBigIntType) + } + default: + if _, ok := right.(*valueBigInt); ok { + panic(errMixBigIntType) + } + ret = floatToValue(left.ToFloat() + right.ToFloat()) + } + } + + vm.stack[vm.sp-2] = ret + vm.sp-- + vm.pc++ +} + +type _sub struct{} + +var sub _sub + +func (_sub) exec(vm *vm) { + right := vm.stack[vm.sp-1] + left := vm.stack[vm.sp-2] + + left = toNumeric(left) + right = toNumeric(right) + + var result Value + + switch left := left.(type) { + case valueInt: + switch right := right.(type) { + case valueInt: + result = intToValue(int64(left) - int64(right)) + goto end + case *valueBigInt: + panic(errMixBigIntType) + } + case valueFloat: + if _, ok := right.(*valueBigInt); ok { + panic(errMixBigIntType) + } + case *valueBigInt: + if right, ok := right.(*valueBigInt); ok { + result = (*valueBigInt)(new(big.Int).Sub((*big.Int)(left), (*big.Int)(right))) + goto end + } + panic(errMixBigIntType) + } + + result = floatToValue(left.ToFloat() - right.ToFloat()) +end: + vm.sp-- + vm.stack[vm.sp-1] = result + vm.pc++ +} + +type _mul struct{} + +var mul _mul + +func (_mul) exec(vm *vm) { + left := toNumeric(vm.stack[vm.sp-2]) + right := toNumeric(vm.stack[vm.sp-1]) + + var result Value + + switch left := left.(type) { + case valueInt: + switch right := right.(type) { + case valueInt: + if left == 0 && right == -1 || left == -1 && right == 0 { + result = _negativeZero + goto end + } + res := left * right + // check for overflow + if left == 0 || right == 0 || res/left == right { + result = intToValue(int64(res)) + goto end + } + case *valueBigInt: + panic(errMixBigIntType) + } + case valueFloat: + if _, ok := right.(*valueBigInt); ok { + panic(errMixBigIntType) + } + case *valueBigInt: + if right, ok := right.(*valueBigInt); ok { + result = (*valueBigInt)(new(big.Int).Mul((*big.Int)(left), (*big.Int)(right))) + goto end + } + panic(errMixBigIntType) + } + + result = floatToValue(left.ToFloat() * right.ToFloat()) + +end: + vm.sp-- + vm.stack[vm.sp-1] = result + vm.pc++ +} + +type _exp struct{} + +var exp _exp + +func (_exp) exec(vm *vm) { + vm.sp-- + x := vm.stack[vm.sp-1] + y := vm.stack[vm.sp] + + x = toNumeric(x) + y = toNumeric(y) + + var result Value + if x, ok := x.(*valueBigInt); ok { + if y, ok := y.(*valueBigInt); ok { + if (*big.Int)(y).Cmp(big.NewInt(0)) < 0 { + panic(vm.r.newError(vm.r.getRangeError(), "exponent must be positive")) + } + result = (*valueBigInt)(new(big.Int).Exp((*big.Int)(x), (*big.Int)(y), nil)) + goto end + } + panic(errMixBigIntType) + } else if _, ok := y.(*valueBigInt); ok { + panic(errMixBigIntType) + } + + result = pow(x, y) +end: + vm.stack[vm.sp-1] = result + vm.pc++ +} + +type _div struct{} + +var div _div + +func (_div) exec(vm *vm) { + leftValue := toNumeric(vm.stack[vm.sp-2]) + rightValue := toNumeric(vm.stack[vm.sp-1]) + + var ( + result Value + left, right float64 + ) + + if left, ok := leftValue.(*valueBigInt); ok { + if right, ok := rightValue.(*valueBigInt); ok { + if (*big.Int)(right).Cmp(big.NewInt(0)) == 0 { + panic(vm.r.newError(vm.r.getRangeError(), "Division by zero")) + } + if (*big.Int)(left).CmpAbs((*big.Int)(right)) < 0 { + result = (*valueBigInt)(big.NewInt(0)) + } else { + i, _ := new(big.Int).QuoRem((*big.Int)(left), (*big.Int)(right), big.NewInt(0)) + result = (*valueBigInt)(i) + } + goto end + } + panic(errMixBigIntType) + } else if _, ok := rightValue.(*valueBigInt); ok { + panic(errMixBigIntType) + } + left, right = leftValue.ToFloat(), rightValue.ToFloat() + + if math.IsNaN(left) || math.IsNaN(right) { + result = _NaN + goto end + } + if math.IsInf(left, 0) && math.IsInf(right, 0) { + result = _NaN + goto end + } + if left == 0 && right == 0 { + result = _NaN + goto end + } + + if math.IsInf(left, 0) { + if math.Signbit(left) == math.Signbit(right) { + result = _positiveInf + goto end + } else { + result = _negativeInf + goto end + } + } + if math.IsInf(right, 0) { + if math.Signbit(left) == math.Signbit(right) { + result = _positiveZero + goto end + } else { + result = _negativeZero + goto end + } + } + if right == 0 { + if math.Signbit(left) == math.Signbit(right) { + result = _positiveInf + goto end + } else { + result = _negativeInf + goto end + } + } + + result = floatToValue(left / right) + +end: + vm.sp-- + vm.stack[vm.sp-1] = result + vm.pc++ +} + +type _mod struct{} + +var mod _mod + +func (_mod) exec(vm *vm) { + left := toNumeric(vm.stack[vm.sp-2]) + right := toNumeric(vm.stack[vm.sp-1]) + + var result Value + + switch left := left.(type) { + case valueInt: + switch right := right.(type) { + case valueInt: + if right == 0 { + result = _NaN + goto end + } + r := left % right + if r == 0 && left < 0 { + result = _negativeZero + } else { + result = intToValue(int64(left % right)) + } + goto end + case *valueBigInt: + panic(errMixBigIntType) + } + case valueFloat: + if _, ok := right.(*valueBigInt); ok { + panic(errMixBigIntType) + } + case *valueBigInt: + if right, ok := right.(*valueBigInt); ok { + switch { + case (*big.Int)(right).Cmp(big.NewInt(0)) == 0: + panic(vm.r.newError(vm.r.getRangeError(), "Division by zero")) + case (*big.Int)(left).Cmp(big.NewInt(0)) < 0: + abs := new(big.Int).Abs((*big.Int)(left)) + v := new(big.Int).Mod(abs, (*big.Int)(right)) + result = (*valueBigInt)(v.Neg(v)) + default: + result = (*valueBigInt)(new(big.Int).Mod((*big.Int)(left), (*big.Int)(right))) + } + goto end + } + panic(errMixBigIntType) + } + + result = floatToValue(math.Mod(left.ToFloat(), right.ToFloat())) +end: + vm.sp-- + vm.stack[vm.sp-1] = result + vm.pc++ +} + +type _neg struct{} + +var neg _neg + +func (_neg) exec(vm *vm) { + operand := vm.stack[vm.sp-1] + + var result Value + + switch n := toNumeric(operand).(type) { + case *valueBigInt: + result = (*valueBigInt)(new(big.Int).Neg((*big.Int)(n))) + case valueInt: + if n == 0 { + result = _negativeZero + } else { + result = -n + } + default: + f := operand.ToFloat() + if !math.IsNaN(f) { + f = -f + } + result = valueFloat(f) + } + + vm.stack[vm.sp-1] = result + vm.pc++ +} + +type _plus struct{} + +var plus _plus + +func (_plus) exec(vm *vm) { + vm.stack[vm.sp-1] = vm.stack[vm.sp-1].ToNumber() + vm.pc++ +} + +type _inc struct{} + +var inc _inc + +func (_inc) exec(vm *vm) { + v := vm.stack[vm.sp-1] + + switch n := v.(type) { + case *valueBigInt: + v = (*valueBigInt)(new(big.Int).Add((*big.Int)(n), big.NewInt(1))) + case valueInt: + v = intToValue(int64(n + 1)) + default: + v = valueFloat(n.ToFloat() + 1) + } + + vm.stack[vm.sp-1] = v + vm.pc++ +} + +type _dec struct{} + +var dec _dec + +func (_dec) exec(vm *vm) { + v := vm.stack[vm.sp-1] + + switch n := v.(type) { + case *valueBigInt: + v = (*valueBigInt)(new(big.Int).Sub((*big.Int)(n), big.NewInt(1))) + case valueInt: + v = intToValue(int64(n - 1)) + default: + v = valueFloat(n.ToFloat() - 1) + } + + vm.stack[vm.sp-1] = v + vm.pc++ +} + +type _and struct{} + +var and _and + +func (_and) exec(vm *vm) { + left := toNumeric(vm.stack[vm.sp-2]) + right := toNumeric(vm.stack[vm.sp-1]) + var result Value + + if left, ok := left.(*valueBigInt); ok { + if right, ok := right.(*valueBigInt); ok { + result = (*valueBigInt)(new(big.Int).And((*big.Int)(left), (*big.Int)(right))) + goto end + } + panic(errMixBigIntType) + } else if _, ok := right.(*valueBigInt); ok { + panic(errMixBigIntType) + } + + result = intToValue(int64(toInt32(left) & toInt32(right))) +end: + vm.stack[vm.sp-2] = result + vm.sp-- + vm.pc++ +} + +type _or struct{} + +var or _or + +func (_or) exec(vm *vm) { + left := toNumeric(vm.stack[vm.sp-2]) + right := toNumeric(vm.stack[vm.sp-1]) + var result Value + + if left, ok := left.(*valueBigInt); ok { + if right, ok := right.(*valueBigInt); ok { + result = (*valueBigInt)(new(big.Int).Or((*big.Int)(left), (*big.Int)(right))) + goto end + } + panic(errMixBigIntType) + } else if _, ok := right.(*valueBigInt); ok { + panic(errMixBigIntType) + } + + result = intToValue(int64(toInt32(left) | toInt32(right))) +end: + vm.stack[vm.sp-2] = result + vm.sp-- + vm.pc++ +} + +type _xor struct{} + +var xor _xor + +func (_xor) exec(vm *vm) { + left := toNumeric(vm.stack[vm.sp-2]) + right := toNumeric(vm.stack[vm.sp-1]) + var result Value + + if left, ok := left.(*valueBigInt); ok { + if right, ok := right.(*valueBigInt); ok { + result = (*valueBigInt)(new(big.Int).Xor((*big.Int)(left), (*big.Int)(right))) + goto end + } + panic(errMixBigIntType) + } else if _, ok := right.(*valueBigInt); ok { + panic(errMixBigIntType) + } + + result = intToValue(int64(toInt32(left) ^ toInt32(right))) +end: + vm.stack[vm.sp-2] = result + vm.sp-- + vm.pc++ +} + +type _bnot struct{} + +var bnot _bnot + +func (_bnot) exec(vm *vm) { + v := vm.stack[vm.sp-1] + switch n := toNumeric(v).(type) { + case *valueBigInt: + v = (*valueBigInt)(new(big.Int).Not((*big.Int)(n))) + default: + v = intToValue(int64(^toInt32(n))) + } + vm.stack[vm.sp-1] = v + vm.pc++ +} + +type _sal struct{} + +var sal _sal + +func (_sal) exec(vm *vm) { + left := toNumeric(vm.stack[vm.sp-2]) + right := toNumeric(vm.stack[vm.sp-1]) + var result Value + + if left, ok := left.(*valueBigInt); ok { + if right, ok := right.(*valueBigInt); ok { + n := uint((*big.Int)(right).Uint64()) + if (*big.Int)(right).Sign() < 0 { + result = (*valueBigInt)(new(big.Int).Rsh((*big.Int)(left), n)) + } else { + result = (*valueBigInt)(new(big.Int).Lsh((*big.Int)(left), n)) + } + goto end + } + panic(errMixBigIntType) + } else if _, ok := right.(*valueBigInt); ok { + panic(errMixBigIntType) + } + + result = intToValue(int64(toInt32(left) << (toUint32(right) & 0x1F))) +end: + vm.stack[vm.sp-2] = result + vm.sp-- + vm.pc++ +} + +type _sar struct{} + +var sar _sar + +func (_sar) exec(vm *vm) { + left := toNumeric(vm.stack[vm.sp-2]) + right := toNumeric(vm.stack[vm.sp-1]) + var result Value + + if left, ok := left.(*valueBigInt); ok { + if right, ok := right.(*valueBigInt); ok { + n := uint((*big.Int)(right).Uint64()) + if (*big.Int)(right).Sign() < 0 { + result = (*valueBigInt)(new(big.Int).Lsh((*big.Int)(left), n)) + } else { + result = (*valueBigInt)(new(big.Int).Rsh((*big.Int)(left), n)) + } + goto end + } + panic(errMixBigIntType) + } else if _, ok := right.(*valueBigInt); ok { + panic(errMixBigIntType) + } + + result = intToValue(int64(toInt32(left) >> (toUint32(right) & 0x1F))) +end: + vm.stack[vm.sp-2] = result + vm.sp-- + vm.pc++ +} + +type _shr struct{} + +var shr _shr + +func (_shr) exec(vm *vm) { + left := toNumeric(vm.stack[vm.sp-2]) + right := toNumeric(vm.stack[vm.sp-1]) + + if _, ok := left.(*valueBigInt); ok { + _ = toNumeric(right) + panic(vm.r.NewTypeError("BigInts have no unsigned right shift, use >> instead")) + } else if _, ok := right.(*valueBigInt); ok { + panic(vm.r.NewTypeError("BigInts have no unsigned right shift, use >> instead")) + } + + vm.stack[vm.sp-2] = intToValue(int64(toUint32(left) >> (toUint32(right) & 0x1F))) + vm.sp-- + vm.pc++ +} + +type jump int32 + +func (j jump) exec(vm *vm) { + vm.pc += int(j) +} + +type _toPropertyKey struct{} + +func (_toPropertyKey) exec(vm *vm) { + p := vm.sp - 1 + vm.stack[p] = toPropertyKey(vm.stack[p]) + vm.pc++ +} + +type _toString struct{} + +func (_toString) exec(vm *vm) { + p := vm.sp - 1 + vm.stack[p] = vm.stack[p].toString() + vm.pc++ +} + +type _getElemRef struct{} + +var getElemRef _getElemRef + +func (_getElemRef) exec(vm *vm) { + obj := vm.stack[vm.sp-2].ToObject(vm.r) + propName := vm.stack[vm.sp-1] + vm.refStack = append(vm.refStack, &objRef{ + base: obj, + name: propName, + }) + vm.sp -= 2 + vm.pc++ +} + +type _getElemRefRecv struct{} + +var getElemRefRecv _getElemRefRecv + +func (_getElemRefRecv) exec(vm *vm) { + obj := vm.stack[vm.sp-1].ToObject(vm.r) + propName := vm.stack[vm.sp-2] + vm.refStack = append(vm.refStack, &objRef{ + base: obj, + name: propName, + this: vm.stack[vm.sp-3], + }) + vm.sp -= 3 + vm.pc++ +} + +type _getElemRefStrict struct{} + +var getElemRefStrict _getElemRefStrict + +func (_getElemRefStrict) exec(vm *vm) { + obj := vm.stack[vm.sp-2].ToObject(vm.r) + propName := vm.stack[vm.sp-1] + vm.refStack = append(vm.refStack, &objRef{ + base: obj, + name: propName, + strict: true, + }) + vm.sp -= 2 + vm.pc++ +} + +type _getElemRefRecvStrict struct{} + +var getElemRefRecvStrict _getElemRefRecvStrict + +func (_getElemRefRecvStrict) exec(vm *vm) { + obj := vm.stack[vm.sp-1].ToObject(vm.r) + propName := vm.stack[vm.sp-2] + vm.refStack = append(vm.refStack, &objRef{ + base: obj, + name: propName, + this: vm.stack[vm.sp-3], + strict: true, + }) + vm.sp -= 3 + vm.pc++ +} + +type _setElem struct{} + +var setElem _setElem + +func (_setElem) exec(vm *vm) { + obj := vm.stack[vm.sp-3].ToObject(vm.r) + propName := toPropertyKey(vm.stack[vm.sp-2]) + val := vm.stack[vm.sp-1] + + obj.setOwn(propName, val, false) + + vm.sp -= 2 + vm.stack[vm.sp-1] = val + vm.pc++ +} + +type _setElem1 struct{} + +var setElem1 _setElem1 + +func (_setElem1) exec(vm *vm) { + obj := vm.stack[vm.sp-3].ToObject(vm.r) + propName := vm.stack[vm.sp-2] + val := vm.stack[vm.sp-1] + + obj.setOwn(propName, val, true) + + vm.sp -= 2 + vm.pc++ +} + +type _setElem1Named struct{} + +var setElem1Named _setElem1Named + +func (_setElem1Named) exec(vm *vm) { + receiver := vm.stack[vm.sp-3] + base := receiver.ToObject(vm.r) + propName := vm.stack[vm.sp-2] + val := vm.stack[vm.sp-1] + vm.r.toObject(val).self.defineOwnPropertyStr("name", PropertyDescriptor{ + Value: funcName("", propName), + Configurable: FLAG_TRUE, + }, true) + base.set(propName, val, receiver, true) + + vm.sp -= 2 + vm.pc++ +} + +type defineMethod struct { + enumerable bool +} + +func (d *defineMethod) exec(vm *vm) { + obj := vm.r.toObject(vm.stack[vm.sp-3]) + propName := vm.stack[vm.sp-2] + method := vm.r.toObject(vm.stack[vm.sp-1]) + method.self.defineOwnPropertyStr("name", PropertyDescriptor{ + Value: funcName("", propName), + Configurable: FLAG_TRUE, + }, true) + obj.defineOwnProperty(propName, PropertyDescriptor{ + Value: method, + Writable: FLAG_TRUE, + Configurable: FLAG_TRUE, + Enumerable: ToFlag(d.enumerable), + }, true) + + vm.sp -= 2 + vm.pc++ +} + +type _setElemP struct{} + +var setElemP _setElemP + +func (_setElemP) exec(vm *vm) { + obj := vm.stack[vm.sp-3].ToObject(vm.r) + propName := toPropertyKey(vm.stack[vm.sp-2]) + val := vm.stack[vm.sp-1] + + obj.setOwn(propName, val, false) + + vm.sp -= 3 + vm.pc++ +} + +type _setElemStrict struct{} + +var setElemStrict _setElemStrict + +func (_setElemStrict) exec(vm *vm) { + propName := toPropertyKey(vm.stack[vm.sp-2]) + receiver := vm.stack[vm.sp-3] + val := vm.stack[vm.sp-1] + if receiverObj, ok := receiver.(*Object); ok { + receiverObj.setOwn(propName, val, true) + } else { + base := receiver.ToObject(vm.r) + base.set(propName, val, receiver, true) + } + + vm.sp -= 2 + vm.stack[vm.sp-1] = val + vm.pc++ +} + +type _setElemRecv struct{} + +var setElemRecv _setElemRecv + +func (_setElemRecv) exec(vm *vm) { + receiver := vm.stack[vm.sp-4] + propName := toPropertyKey(vm.stack[vm.sp-3]) + o := vm.stack[vm.sp-2] + val := vm.stack[vm.sp-1] + if obj, ok := o.(*Object); ok { + obj.set(propName, val, receiver, false) + } else { + base := o.ToObject(vm.r) + base.set(propName, val, receiver, false) + } + + vm.sp -= 3 + vm.stack[vm.sp-1] = val + vm.pc++ +} + +type _setElemRecvStrict struct{} + +var setElemRecvStrict _setElemRecvStrict + +func (_setElemRecvStrict) exec(vm *vm) { + receiver := vm.stack[vm.sp-4] + propName := toPropertyKey(vm.stack[vm.sp-3]) + o := vm.stack[vm.sp-2] + val := vm.stack[vm.sp-1] + if obj, ok := o.(*Object); ok { + obj.set(propName, val, receiver, true) + } else { + base := o.ToObject(vm.r) + base.set(propName, val, receiver, true) + } + + vm.sp -= 3 + vm.stack[vm.sp-1] = val + vm.pc++ +} + +type _setElemStrictP struct{} + +var setElemStrictP _setElemStrictP + +func (_setElemStrictP) exec(vm *vm) { + propName := toPropertyKey(vm.stack[vm.sp-2]) + receiver := vm.stack[vm.sp-3] + val := vm.stack[vm.sp-1] + if receiverObj, ok := receiver.(*Object); ok { + receiverObj.setOwn(propName, val, true) + } else { + base := receiver.ToObject(vm.r) + base.set(propName, val, receiver, true) + } + + vm.sp -= 3 + vm.pc++ +} + +type _setElemRecvP struct{} + +var setElemRecvP _setElemRecvP + +func (_setElemRecvP) exec(vm *vm) { + receiver := vm.stack[vm.sp-4] + propName := toPropertyKey(vm.stack[vm.sp-3]) + o := vm.stack[vm.sp-2] + val := vm.stack[vm.sp-1] + if obj, ok := o.(*Object); ok { + obj.set(propName, val, receiver, false) + } else { + base := o.ToObject(vm.r) + base.set(propName, val, receiver, false) + } + + vm.sp -= 4 + vm.pc++ +} + +type _setElemRecvStrictP struct{} + +var setElemRecvStrictP _setElemRecvStrictP + +func (_setElemRecvStrictP) exec(vm *vm) { + receiver := vm.stack[vm.sp-4] + propName := toPropertyKey(vm.stack[vm.sp-3]) + o := vm.stack[vm.sp-2] + val := vm.stack[vm.sp-1] + if obj, ok := o.(*Object); ok { + obj.set(propName, val, receiver, true) + } else { + base := o.ToObject(vm.r) + base.set(propName, val, receiver, true) + } + + vm.sp -= 4 + vm.pc++ +} + +type _deleteElem struct{} + +var deleteElem _deleteElem + +func (_deleteElem) exec(vm *vm) { + obj := vm.stack[vm.sp-2].ToObject(vm.r) + propName := toPropertyKey(vm.stack[vm.sp-1]) + if obj.delete(propName, false) { + vm.stack[vm.sp-2] = valueTrue + } else { + vm.stack[vm.sp-2] = valueFalse + } + vm.sp-- + vm.pc++ +} + +type _deleteElemStrict struct{} + +var deleteElemStrict _deleteElemStrict + +func (_deleteElemStrict) exec(vm *vm) { + obj := vm.stack[vm.sp-2].ToObject(vm.r) + propName := toPropertyKey(vm.stack[vm.sp-1]) + obj.delete(propName, true) + vm.stack[vm.sp-2] = valueTrue + vm.sp-- + vm.pc++ +} + +type deleteProp unistring.String + +func (d deleteProp) exec(vm *vm) { + obj := vm.stack[vm.sp-1].ToObject(vm.r) + if obj.self.deleteStr(unistring.String(d), false) { + vm.stack[vm.sp-1] = valueTrue + } else { + vm.stack[vm.sp-1] = valueFalse + } + vm.pc++ +} + +type deletePropStrict unistring.String + +func (d deletePropStrict) exec(vm *vm) { + obj := vm.stack[vm.sp-1].ToObject(vm.r) + obj.self.deleteStr(unistring.String(d), true) + vm.stack[vm.sp-1] = valueTrue + vm.pc++ +} + +type getPropRef unistring.String + +func (p getPropRef) exec(vm *vm) { + vm.refStack = append(vm.refStack, &objStrRef{ + base: vm.stack[vm.sp-1].ToObject(vm.r), + name: unistring.String(p), + }) + vm.sp-- + vm.pc++ +} + +type getPropRefRecv unistring.String + +func (p getPropRefRecv) exec(vm *vm) { + vm.refStack = append(vm.refStack, &objStrRef{ + this: vm.stack[vm.sp-2], + base: vm.stack[vm.sp-1].ToObject(vm.r), + name: unistring.String(p), + }) + vm.sp -= 2 + vm.pc++ +} + +type getPropRefStrict unistring.String + +func (p getPropRefStrict) exec(vm *vm) { + vm.refStack = append(vm.refStack, &objStrRef{ + base: vm.stack[vm.sp-1].ToObject(vm.r), + name: unistring.String(p), + strict: true, + }) + vm.sp-- + vm.pc++ +} + +type getPropRefRecvStrict unistring.String + +func (p getPropRefRecvStrict) exec(vm *vm) { + vm.refStack = append(vm.refStack, &objStrRef{ + this: vm.stack[vm.sp-2], + base: vm.stack[vm.sp-1].ToObject(vm.r), + name: unistring.String(p), + strict: true, + }) + vm.sp -= 2 + vm.pc++ +} + +type setProp unistring.String + +func (p setProp) exec(vm *vm) { + val := vm.stack[vm.sp-1] + vm.stack[vm.sp-2].ToObject(vm.r).self.setOwnStr(unistring.String(p), val, false) + vm.stack[vm.sp-2] = val + vm.sp-- + vm.pc++ +} + +type setPropP unistring.String + +func (p setPropP) exec(vm *vm) { + val := vm.stack[vm.sp-1] + vm.stack[vm.sp-2].ToObject(vm.r).self.setOwnStr(unistring.String(p), val, false) + vm.sp -= 2 + vm.pc++ +} + +type setPropStrict unistring.String + +func (p setPropStrict) exec(vm *vm) { + receiver := vm.stack[vm.sp-2] + val := vm.stack[vm.sp-1] + propName := unistring.String(p) + if receiverObj, ok := receiver.(*Object); ok { + receiverObj.self.setOwnStr(propName, val, true) + } else { + base := receiver.ToObject(vm.r) + base.setStr(propName, val, receiver, true) + } + + vm.stack[vm.sp-2] = val + vm.sp-- + vm.pc++ +} + +type setPropRecv unistring.String + +func (p setPropRecv) exec(vm *vm) { + receiver := vm.stack[vm.sp-3] + o := vm.stack[vm.sp-2] + val := vm.stack[vm.sp-1] + propName := unistring.String(p) + if obj, ok := o.(*Object); ok { + obj.setStr(propName, val, receiver, false) + } else { + base := o.ToObject(vm.r) + base.setStr(propName, val, receiver, false) + } + + vm.stack[vm.sp-3] = val + vm.sp -= 2 + vm.pc++ +} + +type setPropRecvStrict unistring.String + +func (p setPropRecvStrict) exec(vm *vm) { + receiver := vm.stack[vm.sp-3] + o := vm.stack[vm.sp-2] + val := vm.stack[vm.sp-1] + propName := unistring.String(p) + if obj, ok := o.(*Object); ok { + obj.setStr(propName, val, receiver, true) + } else { + base := o.ToObject(vm.r) + base.setStr(propName, val, receiver, true) + } + + vm.stack[vm.sp-3] = val + vm.sp -= 2 + vm.pc++ +} + +type setPropRecvP unistring.String + +func (p setPropRecvP) exec(vm *vm) { + receiver := vm.stack[vm.sp-3] + o := vm.stack[vm.sp-2] + val := vm.stack[vm.sp-1] + propName := unistring.String(p) + if obj, ok := o.(*Object); ok { + obj.setStr(propName, val, receiver, false) + } else { + base := o.ToObject(vm.r) + base.setStr(propName, val, receiver, false) + } + + vm.sp -= 3 + vm.pc++ +} + +type setPropRecvStrictP unistring.String + +func (p setPropRecvStrictP) exec(vm *vm) { + receiver := vm.stack[vm.sp-3] + o := vm.stack[vm.sp-2] + val := vm.stack[vm.sp-1] + propName := unistring.String(p) + if obj, ok := o.(*Object); ok { + obj.setStr(propName, val, receiver, true) + } else { + base := o.ToObject(vm.r) + base.setStr(propName, val, receiver, true) + } + + vm.sp -= 3 + vm.pc++ +} + +type setPropStrictP unistring.String + +func (p setPropStrictP) exec(vm *vm) { + receiver := vm.stack[vm.sp-2] + val := vm.stack[vm.sp-1] + propName := unistring.String(p) + if receiverObj, ok := receiver.(*Object); ok { + receiverObj.self.setOwnStr(propName, val, true) + } else { + base := receiver.ToObject(vm.r) + base.setStr(propName, val, receiver, true) + } + + vm.sp -= 2 + vm.pc++ +} + +type putProp unistring.String + +func (p putProp) exec(vm *vm) { + vm.r.toObject(vm.stack[vm.sp-2]).self._putProp(unistring.String(p), vm.stack[vm.sp-1], true, true, true) + + vm.sp-- + vm.pc++ +} + +// used in class declarations instead of putProp because DefineProperty must be observable by Proxy +type definePropKeyed unistring.String + +func (p definePropKeyed) exec(vm *vm) { + vm.r.toObject(vm.stack[vm.sp-2]).self.defineOwnPropertyStr(unistring.String(p), PropertyDescriptor{ + Value: vm.stack[vm.sp-1], + Writable: FLAG_TRUE, + Configurable: FLAG_TRUE, + Enumerable: FLAG_TRUE, + }, true) + + vm.sp-- + vm.pc++ +} + +type defineProp struct{} + +func (defineProp) exec(vm *vm) { + vm.r.toObject(vm.stack[vm.sp-3]).defineOwnProperty(vm.stack[vm.sp-2], PropertyDescriptor{ + Value: vm.stack[vm.sp-1], + Writable: FLAG_TRUE, + Configurable: FLAG_TRUE, + Enumerable: FLAG_TRUE, + }, true) + + vm.sp -= 2 + vm.pc++ +} + +type defineMethodKeyed struct { + key unistring.String + enumerable bool +} + +func (d *defineMethodKeyed) exec(vm *vm) { + obj := vm.r.toObject(vm.stack[vm.sp-2]) + method := vm.r.toObject(vm.stack[vm.sp-1]) + + obj.self.defineOwnPropertyStr(d.key, PropertyDescriptor{ + Value: method, + Writable: FLAG_TRUE, + Configurable: FLAG_TRUE, + Enumerable: ToFlag(d.enumerable), + }, true) + + vm.sp-- + vm.pc++ +} + +type _setProto struct{} + +var setProto _setProto + +func (_setProto) exec(vm *vm) { + vm.r.setObjectProto(vm.stack[vm.sp-2], vm.stack[vm.sp-1]) + + vm.sp-- + vm.pc++ +} + +type defineGetterKeyed struct { + key unistring.String + enumerable bool +} + +func (s *defineGetterKeyed) exec(vm *vm) { + obj := vm.r.toObject(vm.stack[vm.sp-2]) + val := vm.stack[vm.sp-1] + method := vm.r.toObject(val) + method.self.defineOwnPropertyStr("name", PropertyDescriptor{ + Value: asciiString("get ").Concat(stringValueFromRaw(s.key)), + Configurable: FLAG_TRUE, + }, true) + descr := PropertyDescriptor{ + Getter: val, + Configurable: FLAG_TRUE, + Enumerable: ToFlag(s.enumerable), + } + + obj.self.defineOwnPropertyStr(s.key, descr, true) + + vm.sp-- + vm.pc++ +} + +type defineSetterKeyed struct { + key unistring.String + enumerable bool +} + +func (s *defineSetterKeyed) exec(vm *vm) { + obj := vm.r.toObject(vm.stack[vm.sp-2]) + val := vm.stack[vm.sp-1] + method := vm.r.toObject(val) + method.self.defineOwnPropertyStr("name", PropertyDescriptor{ + Value: asciiString("set ").Concat(stringValueFromRaw(s.key)), + Configurable: FLAG_TRUE, + }, true) + + descr := PropertyDescriptor{ + Setter: val, + Configurable: FLAG_TRUE, + Enumerable: ToFlag(s.enumerable), + } + + obj.self.defineOwnPropertyStr(s.key, descr, true) + + vm.sp-- + vm.pc++ +} + +type defineGetter struct { + enumerable bool +} + +func (s *defineGetter) exec(vm *vm) { + obj := vm.r.toObject(vm.stack[vm.sp-3]) + propName := vm.stack[vm.sp-2] + val := vm.stack[vm.sp-1] + method := vm.r.toObject(val) + method.self.defineOwnPropertyStr("name", PropertyDescriptor{ + Value: funcName("get ", propName), + Configurable: FLAG_TRUE, + }, true) + + descr := PropertyDescriptor{ + Getter: val, + Configurable: FLAG_TRUE, + Enumerable: ToFlag(s.enumerable), + } + + obj.defineOwnProperty(propName, descr, true) + + vm.sp -= 2 + vm.pc++ +} + +type defineSetter struct { + enumerable bool +} + +func (s *defineSetter) exec(vm *vm) { + obj := vm.r.toObject(vm.stack[vm.sp-3]) + propName := vm.stack[vm.sp-2] + val := vm.stack[vm.sp-1] + method := vm.r.toObject(val) + + method.self.defineOwnPropertyStr("name", PropertyDescriptor{ + Value: funcName("set ", propName), + Configurable: FLAG_TRUE, + }, true) + + descr := PropertyDescriptor{ + Setter: val, + Configurable: FLAG_TRUE, + Enumerable: FLAG_TRUE, + } + + obj.defineOwnProperty(propName, descr, true) + + vm.sp -= 2 + vm.pc++ +} + +type getProp unistring.String + +func (g getProp) exec(vm *vm) { + v := vm.stack[vm.sp-1] + obj := v.baseObject(vm.r) + if obj == nil { + vm.throw(vm.r.NewTypeError("Cannot read property '%s' of undefined", g)) + return + } + vm.stack[vm.sp-1] = nilSafe(obj.self.getStr(unistring.String(g), v)) + + vm.pc++ +} + +type getPropRecv unistring.String + +func (g getPropRecv) exec(vm *vm) { + recv := vm.stack[vm.sp-2] + v := vm.stack[vm.sp-1] + obj := v.baseObject(vm.r) + if obj == nil { + vm.throw(vm.r.NewTypeError("Cannot read property '%s' of undefined", g)) + return + } + vm.stack[vm.sp-2] = nilSafe(obj.self.getStr(unistring.String(g), recv)) + vm.sp-- + vm.pc++ +} + +type getPropRecvCallee unistring.String + +func (g getPropRecvCallee) exec(vm *vm) { + recv := vm.stack[vm.sp-2] + v := vm.stack[vm.sp-1] + obj := v.baseObject(vm.r) + if obj == nil { + vm.throw(vm.r.NewTypeError("Cannot read property '%s' of undefined", g)) + return + } + + n := unistring.String(g) + prop := obj.self.getStr(n, recv) + if prop == nil { + prop = memberUnresolved{valueUnresolved{r: vm.r, ref: n}} + } + + vm.stack[vm.sp-1] = prop + vm.pc++ +} + +type getPropCallee unistring.String + +func (g getPropCallee) exec(vm *vm) { + v := vm.stack[vm.sp-1] + obj := v.baseObject(vm.r) + n := unistring.String(g) + if obj == nil { + vm.throw(vm.r.NewTypeError("Cannot read property '%s' of undefined or null", n)) + return + } + prop := obj.self.getStr(n, v) + if prop == nil { + prop = memberUnresolved{valueUnresolved{r: vm.r, ref: n}} + } + vm.push(prop) + + vm.pc++ +} + +type _getElem struct{} + +var getElem _getElem + +func (_getElem) exec(vm *vm) { + v := vm.stack[vm.sp-2] + obj := v.baseObject(vm.r) + if obj == nil { + vm.throw(vm.r.NewTypeError("Cannot read property '%s' of undefined", vm.stack[vm.sp-1])) + return + } + propName := toPropertyKey(vm.stack[vm.sp-1]) + + vm.stack[vm.sp-2] = nilSafe(obj.get(propName, v)) + + vm.sp-- + vm.pc++ +} + +type _getElemRecv struct{} + +var getElemRecv _getElemRecv + +func (_getElemRecv) exec(vm *vm) { + recv := vm.stack[vm.sp-3] + v := vm.stack[vm.sp-1] + obj := v.baseObject(vm.r) + if obj == nil { + vm.throw(vm.r.NewTypeError("Cannot read property '%s' of undefined", vm.stack[vm.sp-2])) + return + } + propName := toPropertyKey(vm.stack[vm.sp-2]) + + vm.stack[vm.sp-3] = nilSafe(obj.get(propName, recv)) + + vm.sp -= 2 + vm.pc++ +} + +type _getKey struct{} + +var getKey _getKey + +func (_getKey) exec(vm *vm) { + v := vm.stack[vm.sp-2] + obj := v.baseObject(vm.r) + propName := vm.stack[vm.sp-1] + if obj == nil { + vm.throw(vm.r.NewTypeError("Cannot read property '%s' of undefined", propName.String())) + return + } + + vm.stack[vm.sp-2] = nilSafe(obj.get(propName, v)) + + vm.sp-- + vm.pc++ +} + +type _getElemCallee struct{} + +var getElemCallee _getElemCallee + +func (_getElemCallee) exec(vm *vm) { + v := vm.stack[vm.sp-2] + obj := v.baseObject(vm.r) + if obj == nil { + vm.throw(vm.r.NewTypeError("Cannot read property '%s' of undefined", vm.stack[vm.sp-1])) + return + } + + propName := toPropertyKey(vm.stack[vm.sp-1]) + prop := obj.get(propName, v) + if prop == nil { + prop = memberUnresolved{valueUnresolved{r: vm.r, ref: propName.string()}} + } + vm.stack[vm.sp-1] = prop + + vm.pc++ +} + +type _getElemRecvCallee struct{} + +var getElemRecvCallee _getElemRecvCallee + +func (_getElemRecvCallee) exec(vm *vm) { + recv := vm.stack[vm.sp-3] + v := vm.stack[vm.sp-2] + obj := v.baseObject(vm.r) + if obj == nil { + vm.throw(vm.r.NewTypeError("Cannot read property '%s' of undefined", vm.stack[vm.sp-1])) + return + } + + propName := toPropertyKey(vm.stack[vm.sp-1]) + prop := obj.get(propName, recv) + if prop == nil { + prop = memberUnresolved{valueUnresolved{r: vm.r, ref: propName.string()}} + } + vm.stack[vm.sp-2] = prop + vm.sp-- + + vm.pc++ +} + +type _dup struct{} + +var dup _dup + +func (_dup) exec(vm *vm) { + vm.push(vm.stack[vm.sp-1]) + vm.pc++ +} + +type dupN uint32 + +func (d dupN) exec(vm *vm) { + vm.push(vm.stack[vm.sp-1-int(d)]) + vm.pc++ +} + +type rdupN uint32 + +func (d rdupN) exec(vm *vm) { + vm.stack[vm.sp-1-int(d)] = vm.stack[vm.sp-1] + vm.pc++ +} + +type dupLast uint32 + +func (d dupLast) exec(vm *vm) { + e := vm.sp + int(d) + vm.stack.expand(e) + copy(vm.stack[vm.sp:e], vm.stack[vm.sp-int(d):]) + vm.sp = e + vm.pc++ +} + +type _newObject struct{} + +var newObject _newObject + +func (_newObject) exec(vm *vm) { + vm.push(vm.r.NewObject()) + vm.pc++ +} + +type newArray uint32 + +func (l newArray) exec(vm *vm) { + values := make([]Value, 0, l) + vm.push(vm.r.newArrayValues(values)) + vm.pc++ +} + +type _pushArrayItem struct{} + +var pushArrayItem _pushArrayItem + +func (_pushArrayItem) exec(vm *vm) { + arr := vm.stack[vm.sp-2].(*Object).self.(*arrayObject) + if arr.length < math.MaxUint32 { + arr.length++ + } else { + vm.throw(vm.r.newError(vm.r.getRangeError(), "Invalid array length")) + return + } + val := vm.stack[vm.sp-1] + arr.values = append(arr.values, val) + if val != nil { + arr.objCount++ + } + vm.sp-- + vm.pc++ +} + +type _pushArraySpread struct{} + +var pushArraySpread _pushArraySpread + +func (_pushArraySpread) exec(vm *vm) { + arr := vm.stack[vm.sp-2].(*Object).self.(*arrayObject) + vm.r.getIterator(vm.stack[vm.sp-1], nil).iterate(func(val Value) { + if arr.length < math.MaxUint32 { + arr.length++ + } else { + vm.throw(vm.r.newError(vm.r.getRangeError(), "Invalid array length")) + return + } + arr.values = append(arr.values, val) + arr.objCount++ + }) + vm.sp-- + vm.pc++ +} + +type _pushSpread struct{} + +var pushSpread _pushSpread + +func (_pushSpread) exec(vm *vm) { + vm.sp-- + obj := vm.stack[vm.sp] + vm.r.getIterator(obj, nil).iterate(func(val Value) { + vm.push(val) + }) + vm.pc++ +} + +type _newArrayFromIter struct{} + +var newArrayFromIter _newArrayFromIter + +func (_newArrayFromIter) exec(vm *vm) { + var values []Value + l := len(vm.iterStack) - 1 + iter := vm.iterStack[l].iter + vm.iterStack[l] = iterStackItem{} + vm.iterStack = vm.iterStack[:l] + if iter.iterator != nil { + iter.iterate(func(val Value) { + values = append(values, val) + }) + } + vm.push(vm.r.newArrayValues(values)) + vm.pc++ +} + +type newRegexp struct { + pattern *regexpPattern + src String +} + +func (n *newRegexp) exec(vm *vm) { + vm.push(vm.r.newRegExpp(n.pattern.clone(), n.src, vm.r.getRegExpPrototype()).val) + vm.pc++ +} + +func (vm *vm) setLocalLex(s int) { + v := vm.stack[vm.sp-1] + level := s >> 24 + idx := uint32(s & 0x00FFFFFF) + stash := vm.stash + for i := 0; i < level; i++ { + stash = stash.outer + } + p := &stash.values[idx] + if *p == nil { + panic(errAccessBeforeInit) + } + *p = v + vm.pc++ +} + +func (vm *vm) initLocal(s int) { + v := vm.stack[vm.sp-1] + level := s >> 24 + idx := uint32(s & 0x00FFFFFF) + stash := vm.stash + for i := 0; i < level; i++ { + stash = stash.outer + } + stash.initByIdx(idx, v) + vm.pc++ +} + +type storeStash uint32 + +func (s storeStash) exec(vm *vm) { + vm.initLocal(int(s)) +} + +type storeStashP uint32 + +func (s storeStashP) exec(vm *vm) { + vm.initLocal(int(s)) + vm.sp-- +} + +type storeStashLex uint32 + +func (s storeStashLex) exec(vm *vm) { + vm.setLocalLex(int(s)) +} + +type storeStashLexP uint32 + +func (s storeStashLexP) exec(vm *vm) { + vm.setLocalLex(int(s)) + vm.sp-- +} + +type initStash uint32 + +func (s initStash) exec(vm *vm) { + vm.initLocal(int(s)) +} + +type initStashP uint32 + +func (s initStashP) exec(vm *vm) { + vm.initLocal(int(s)) + vm.sp-- +} + +type initGlobalP unistring.String + +func (s initGlobalP) exec(vm *vm) { + vm.sp-- + vm.r.global.stash.initByName(unistring.String(s), vm.stack[vm.sp]) + vm.pc++ +} + +type initGlobal unistring.String + +func (s initGlobal) exec(vm *vm) { + vm.r.global.stash.initByName(unistring.String(s), vm.stack[vm.sp]) + vm.pc++ +} + +type resolveVar1 unistring.String + +func (s resolveVar1) exec(vm *vm) { + name := unistring.String(s) + var ref ref + for stash := vm.stash; stash != nil; stash = stash.outer { + ref = stash.getRefByName(name, false) + if ref != nil { + goto end + } + } + + ref = &objStrRef{ + base: vm.r.globalObject, + name: name, + binding: true, + } + +end: + vm.refStack = append(vm.refStack, ref) + vm.pc++ +} + +type deleteVar unistring.String + +func (d deleteVar) exec(vm *vm) { + name := unistring.String(d) + ret := true + for stash := vm.stash; stash != nil; stash = stash.outer { + if stash.obj != nil { + if stashObjHas(stash.obj, name) { + ret = stash.obj.self.deleteStr(name, false) + goto end + } + } else { + if idx, exists := stash.names[name]; exists { + if idx&(maskVar|maskDeletable) == maskVar|maskDeletable { + stash.deleteBinding(name) + } else { + ret = false + } + goto end + } + } + } + + if vm.r.globalObject.self.hasPropertyStr(name) { + ret = vm.r.globalObject.self.deleteStr(name, false) + } + +end: + if ret { + vm.push(valueTrue) + } else { + vm.push(valueFalse) + } + vm.pc++ +} + +type deleteGlobal unistring.String + +func (d deleteGlobal) exec(vm *vm) { + name := unistring.String(d) + var ret bool + if vm.r.globalObject.self.hasPropertyStr(name) { + ret = vm.r.globalObject.self.deleteStr(name, false) + } else { + ret = true + } + if ret { + vm.push(valueTrue) + } else { + vm.push(valueFalse) + } + vm.pc++ +} + +type resolveVar1Strict unistring.String + +func (s resolveVar1Strict) exec(vm *vm) { + name := unistring.String(s) + var ref ref + for stash := vm.stash; stash != nil; stash = stash.outer { + ref = stash.getRefByName(name, true) + if ref != nil { + goto end + } + } + + if vm.r.globalObject.self.hasPropertyStr(name) { + ref = &objStrRef{ + base: vm.r.globalObject, + name: name, + binding: true, + strict: true, + } + goto end + } + + ref = &unresolvedRef{ + runtime: vm.r, + name: name, + } + +end: + vm.refStack = append(vm.refStack, ref) + vm.pc++ +} + +type setGlobal unistring.String + +func (s setGlobal) exec(vm *vm) { + vm.r.setGlobal(unistring.String(s), vm.peek(), false) + vm.pc++ +} + +type setGlobalStrict unistring.String + +func (s setGlobalStrict) exec(vm *vm) { + vm.r.setGlobal(unistring.String(s), vm.peek(), true) + vm.pc++ +} + +// Load a var from stash +type loadStash uint32 + +func (g loadStash) exec(vm *vm) { + level := int(g >> 24) + idx := uint32(g & 0x00FFFFFF) + stash := vm.stash + for i := 0; i < level; i++ { + stash = stash.outer + } + + vm.push(nilSafe(stash.getByIdx(idx))) + vm.pc++ +} + +// Load a lexical binding from stash +type loadStashLex uint32 + +func (g loadStashLex) exec(vm *vm) { + level := int(g >> 24) + idx := uint32(g & 0x00FFFFFF) + stash := vm.stash + for i := 0; i < level; i++ { + stash = stash.outer + } + + v := stash.getByIdx(idx) + if v == nil { + vm.throw(errAccessBeforeInit) + return + } + vm.push(v) + vm.pc++ +} + +// scan dynamic stashes up to the given level (encoded as 8 most significant bits of idx), if not found +// return the indexed var binding value from stash +type loadMixed struct { + name unistring.String + idx uint32 + callee bool +} + +func (g *loadMixed) exec(vm *vm) { + level := int(g.idx >> 24) + idx := g.idx & 0x00FFFFFF + stash := vm.stash + name := g.name + for i := 0; i < level; i++ { + if v, found := stash.getByName(name); found { + if g.callee { + if stash.obj != nil { + vm.push(stash.obj) + } else { + vm.push(_undefined) + } + } + vm.push(v) + goto end + } + stash = stash.outer + } + if g.callee { + vm.push(_undefined) + } + if stash != nil { + vm.push(nilSafe(stash.getByIdx(idx))) + } +end: + vm.pc++ +} + +// scan dynamic stashes up to the given level (encoded as 8 most significant bits of idx), if not found +// return the indexed lexical binding value from stash +type loadMixedLex loadMixed + +func (g *loadMixedLex) exec(vm *vm) { + level := int(g.idx >> 24) + idx := g.idx & 0x00FFFFFF + stash := vm.stash + name := g.name + for i := 0; i < level; i++ { + if v, found := stash.getByName(name); found { + if g.callee { + if stash.obj != nil { + vm.push(stash.obj) + } else { + vm.push(_undefined) + } + } + vm.push(v) + goto end + } + stash = stash.outer + } + if g.callee { + vm.push(_undefined) + } + if stash != nil { + v := stash.getByIdx(idx) + if v == nil { + vm.throw(errAccessBeforeInit) + return + } + vm.push(v) + } +end: + vm.pc++ +} + +// scan dynamic stashes up to the given level (encoded as 8 most significant bits of idx), if not found +// return the indexed var binding value from stack +type loadMixedStack struct { + name unistring.String + idx int + level uint8 + callee bool +} + +// same as loadMixedStack, but the args have been moved to stash (therefore stack layout is different) +type loadMixedStack1 loadMixedStack + +func (g *loadMixedStack) exec(vm *vm) { + stash := vm.stash + name := g.name + level := int(g.level) + for i := 0; i < level; i++ { + if v, found := stash.getByName(name); found { + if g.callee { + if stash.obj != nil { + vm.push(stash.obj) + } else { + vm.push(_undefined) + } + } + vm.push(v) + goto end + } + stash = stash.outer + } + if g.callee { + vm.push(_undefined) + } + loadStack(g.idx).exec(vm) + return +end: + vm.pc++ +} + +func (g *loadMixedStack1) exec(vm *vm) { + stash := vm.stash + name := g.name + level := int(g.level) + for i := 0; i < level; i++ { + if v, found := stash.getByName(name); found { + if g.callee { + if stash.obj != nil { + vm.push(stash.obj) + } else { + vm.push(_undefined) + } + } + vm.push(v) + goto end + } + stash = stash.outer + } + if g.callee { + vm.push(_undefined) + } + loadStack1(g.idx).exec(vm) + return +end: + vm.pc++ +} + +type loadMixedStackLex loadMixedStack + +// same as loadMixedStackLex but when the arguments have been moved into stash +type loadMixedStack1Lex loadMixedStack + +func (g *loadMixedStackLex) exec(vm *vm) { + stash := vm.stash + name := g.name + level := int(g.level) + for i := 0; i < level; i++ { + if v, found := stash.getByName(name); found { + if g.callee { + if stash.obj != nil { + vm.push(stash.obj) + } else { + vm.push(_undefined) + } + } + vm.push(v) + goto end + } + stash = stash.outer + } + if g.callee { + vm.push(_undefined) + } + loadStackLex(g.idx).exec(vm) + return +end: + vm.pc++ +} + +func (g *loadMixedStack1Lex) exec(vm *vm) { + stash := vm.stash + name := g.name + level := int(g.level) + for i := 0; i < level; i++ { + if v, found := stash.getByName(name); found { + if g.callee { + if stash.obj != nil { + vm.push(stash.obj) + } else { + vm.push(_undefined) + } + } + vm.push(v) + goto end + } + stash = stash.outer + } + if g.callee { + vm.push(_undefined) + } + loadStack1Lex(g.idx).exec(vm) + return +end: + vm.pc++ +} + +type resolveMixed struct { + name unistring.String + idx uint32 + typ varType + strict bool +} + +func newStashRef(typ varType, name unistring.String, v *[]Value, idx int) ref { + switch typ { + case varTypeVar: + return &stashRef{ + n: name, + v: v, + idx: idx, + } + case varTypeLet: + return &stashRefLex{ + stashRef: stashRef{ + n: name, + v: v, + idx: idx, + }, + } + case varTypeConst, varTypeStrictConst: + return &stashRefConst{ + stashRefLex: stashRefLex{ + stashRef: stashRef{ + n: name, + v: v, + idx: idx, + }, + }, + strictConst: typ == varTypeStrictConst, + } + } + panic("unsupported var type") +} + +func (r *resolveMixed) exec(vm *vm) { + level := int(r.idx >> 24) + idx := r.idx & 0x00FFFFFF + stash := vm.stash + var ref ref + for i := 0; i < level; i++ { + ref = stash.getRefByName(r.name, r.strict) + if ref != nil { + goto end + } + stash = stash.outer + } + + if stash != nil { + ref = newStashRef(r.typ, r.name, &stash.values, int(idx)) + goto end + } + + ref = &unresolvedRef{ + runtime: vm.r, + name: r.name, + } + +end: + vm.refStack = append(vm.refStack, ref) + vm.pc++ +} + +type resolveMixedStack struct { + name unistring.String + idx int + typ varType + level uint8 + strict bool +} + +type resolveMixedStack1 resolveMixedStack + +func (r *resolveMixedStack) exec(vm *vm) { + level := int(r.level) + stash := vm.stash + var ref ref + var idx int + for i := 0; i < level; i++ { + ref = stash.getRefByName(r.name, r.strict) + if ref != nil { + goto end + } + stash = stash.outer + } + + if r.idx > 0 { + idx = vm.sb + vm.args + r.idx + } else { + idx = vm.sb - r.idx + } + + ref = newStashRef(r.typ, r.name, (*[]Value)(&vm.stack), idx) + +end: + vm.refStack = append(vm.refStack, ref) + vm.pc++ +} + +func (r *resolveMixedStack1) exec(vm *vm) { + level := int(r.level) + stash := vm.stash + var ref ref + for i := 0; i < level; i++ { + ref = stash.getRefByName(r.name, r.strict) + if ref != nil { + goto end + } + stash = stash.outer + } + + ref = newStashRef(r.typ, r.name, (*[]Value)(&vm.stack), vm.sb+r.idx) + +end: + vm.refStack = append(vm.refStack, ref) + vm.pc++ +} + +type _getValue struct{} + +var getValue _getValue + +func (_getValue) exec(vm *vm) { + ref := vm.refStack[len(vm.refStack)-1] + if v := ref.get(); v != nil { + vm.push(v) + } else { + vm.throw(vm.r.newReferenceError(ref.refname())) + return + } + vm.pc++ +} + +type _putValue struct{} + +var putValue _putValue + +func (_putValue) exec(vm *vm) { + l := len(vm.refStack) - 1 + ref := vm.refStack[l] + vm.refStack[l] = nil + vm.refStack = vm.refStack[:l] + ref.set(vm.stack[vm.sp-1]) + vm.pc++ +} + +type _putValueP struct{} + +var putValueP _putValueP + +func (_putValueP) exec(vm *vm) { + l := len(vm.refStack) - 1 + ref := vm.refStack[l] + vm.refStack[l] = nil + vm.refStack = vm.refStack[:l] + ref.set(vm.stack[vm.sp-1]) + vm.sp-- + vm.pc++ +} + +type _initValueP struct{} + +var initValueP _initValueP + +func (_initValueP) exec(vm *vm) { + l := len(vm.refStack) - 1 + ref := vm.refStack[l] + vm.refStack[l] = nil + vm.refStack = vm.refStack[:l] + ref.init(vm.stack[vm.sp-1]) + vm.sp-- + vm.pc++ +} + +type loadDynamic unistring.String + +func (n loadDynamic) exec(vm *vm) { + name := unistring.String(n) + var val Value + for stash := vm.stash; stash != nil; stash = stash.outer { + if v, exists := stash.getByName(name); exists { + val = v + break + } + } + if val == nil { + val = vm.r.globalObject.self.getStr(name, nil) + if val == nil { + vm.throw(vm.r.newReferenceError(name)) + return + } + } + vm.push(val) + vm.pc++ +} + +type loadDynamicRef unistring.String + +func (n loadDynamicRef) exec(vm *vm) { + name := unistring.String(n) + var val Value + for stash := vm.stash; stash != nil; stash = stash.outer { + if v, exists := stash.getByName(name); exists { + val = v + break + } + } + if val == nil { + val = vm.r.globalObject.self.getStr(name, nil) + if val == nil { + val = valueUnresolved{r: vm.r, ref: name} + } + } + vm.push(val) + vm.pc++ +} + +type loadDynamicCallee unistring.String + +func (n loadDynamicCallee) exec(vm *vm) { + name := unistring.String(n) + var val Value + var callee *Object + for stash := vm.stash; stash != nil; stash = stash.outer { + if v, exists := stash.getByName(name); exists { + callee = stash.obj + val = v + break + } + } + if val == nil { + val = vm.r.globalObject.self.getStr(name, nil) + if val == nil { + val = valueUnresolved{r: vm.r, ref: name} + } + } + if callee != nil { + vm.push(callee) + } else { + vm.push(_undefined) + } + vm.push(val) + vm.pc++ +} + +type _pop struct{} + +var pop _pop + +func (_pop) exec(vm *vm) { + vm.sp-- + vm.pc++ +} + +func (vm *vm) callEval(n int, strict bool) { + if vm.r.toObject(vm.stack[vm.sp-n-1]) == vm.r.global.Eval { + if n > 0 { + srcVal := vm.stack[vm.sp-n] + if src, ok := srcVal.(String); ok { + ret := vm.r.eval(src, true, strict) + vm.stack[vm.sp-n-2] = ret + } else { + vm.stack[vm.sp-n-2] = srcVal + } + } else { + vm.stack[vm.sp-n-2] = _undefined + } + + vm.sp -= n + 1 + vm.pc++ + } else { + call(n).exec(vm) + } +} + +type callEval uint32 + +func (numargs callEval) exec(vm *vm) { + vm.callEval(int(numargs), false) +} + +type callEvalStrict uint32 + +func (numargs callEvalStrict) exec(vm *vm) { + vm.callEval(int(numargs), true) +} + +type _callEvalVariadic struct{} + +var callEvalVariadic _callEvalVariadic + +func (_callEvalVariadic) exec(vm *vm) { + vm.callEval(vm.countVariadicArgs()-2, false) +} + +type _callEvalVariadicStrict struct{} + +var callEvalVariadicStrict _callEvalVariadicStrict + +func (_callEvalVariadicStrict) exec(vm *vm) { + vm.callEval(vm.countVariadicArgs()-2, true) +} + +type _boxThis struct{} + +var boxThis _boxThis + +func (_boxThis) exec(vm *vm) { + v := vm.stack[vm.sb] + if v == _undefined || v == _null { + vm.stack[vm.sb] = vm.r.globalObject + } else { + vm.stack[vm.sb] = v.ToObject(vm.r) + } + vm.pc++ +} + +var variadicMarker Value = newSymbol(asciiString("[variadic marker]")) + +type _startVariadic struct{} + +var startVariadic _startVariadic + +func (_startVariadic) exec(vm *vm) { + vm.push(variadicMarker) + vm.pc++ +} + +type _callVariadic struct{} + +var callVariadic _callVariadic + +func (vm *vm) countVariadicArgs() int { + count := 0 + for i := vm.sp - 1; i >= 0; i-- { + if vm.stack[i] == variadicMarker { + return count + } + count++ + } + panic("Variadic marker was not found. Compiler bug.") +} + +func (_callVariadic) exec(vm *vm) { + call(vm.countVariadicArgs() - 2).exec(vm) +} + +type _endVariadic struct{} + +var endVariadic _endVariadic + +func (_endVariadic) exec(vm *vm) { + vm.sp-- + vm.stack[vm.sp-1] = vm.stack[vm.sp] + vm.pc++ +} + +type call uint32 + +func (numargs call) exec(vm *vm) { + // this + // callee + // arg0 + // ... + // arg + n := int(numargs) + v := vm.stack[vm.sp-n-1] // callee + obj := vm.toCallee(v) + obj.self.vmCall(vm, n) +} + +func (vm *vm) clearStack() { + sp := vm.sp + stackTail := vm.stack[sp:] + for i := range stackTail { + stackTail[i] = nil + } + vm.stack = vm.stack[:sp] +} + +type enterBlock struct { + names map[unistring.String]uint32 + stashSize uint32 + stackSize uint32 +} + +func (e *enterBlock) exec(vm *vm) { + if e.stashSize > 0 { + vm.newStash() + vm.stash.values = make([]Value, e.stashSize) + if len(e.names) > 0 { + vm.stash.names = e.names + } + } + ss := int(e.stackSize) + vm.stack.expand(vm.sp + ss - 1) + vv := vm.stack[vm.sp : vm.sp+ss] + for i := range vv { + vv[i] = nil + } + vm.sp += ss + vm.pc++ +} + +type enterCatchBlock struct { + names map[unistring.String]uint32 + stashSize uint32 + stackSize uint32 +} + +func (e *enterCatchBlock) exec(vm *vm) { + vm.newStash() + vm.stash.values = make([]Value, e.stashSize) + if len(e.names) > 0 { + vm.stash.names = e.names + } + vm.sp-- + vm.stash.values[0] = vm.stack[vm.sp] + ss := int(e.stackSize) + vm.stack.expand(vm.sp + ss - 1) + vv := vm.stack[vm.sp : vm.sp+ss] + for i := range vv { + vv[i] = nil + } + vm.sp += ss + vm.pc++ +} + +type leaveBlock struct { + stackSize uint32 + popStash bool +} + +func (l *leaveBlock) exec(vm *vm) { + if l.popStash { + vm.stash = vm.stash.outer + } + if ss := l.stackSize; ss > 0 { + vm.sp -= int(ss) + } + vm.pc++ +} + +type enterFunc struct { + names map[unistring.String]uint32 + stashSize uint32 + stackSize uint32 + numArgs uint32 + funcType funcType + argsToStash bool + extensible bool +} + +func (e *enterFunc) exec(vm *vm) { + // Input stack: + // + // callee + // this + // arg0 + // ... + // argN + // <- sp + + // Output stack: + // + // this <- sb + // + // <- sp + sp := vm.sp + vm.sb = sp - vm.args - 1 + vm.newStash() + stash := vm.stash + stash.funcType = e.funcType + stash.values = make([]Value, e.stashSize) + if len(e.names) > 0 { + if e.extensible { + m := make(map[unistring.String]uint32, len(e.names)) + for name, idx := range e.names { + m[name] = idx + } + stash.names = m + } else { + stash.names = e.names + } + } + + ss := int(e.stackSize) + ea := 0 + if e.argsToStash { + offset := vm.args - int(e.numArgs) + copy(stash.values, vm.stack[sp-vm.args:sp]) + if offset > 0 { + vm.stash.extraArgs = make([]Value, offset) + copy(stash.extraArgs, vm.stack[sp-offset:]) + } else { + vv := stash.values[vm.args:e.numArgs] + for i := range vv { + vv[i] = _undefined + } + } + sp -= vm.args + } else { + d := int(e.numArgs) - vm.args + if d > 0 { + ss += d + ea = d + vm.args = int(e.numArgs) + } + } + vm.stack.expand(sp + ss - 1) + if ea > 0 { + vv := vm.stack[sp : vm.sp+ea] + for i := range vv { + vv[i] = _undefined + } + } + vv := vm.stack[sp+ea : sp+ss] + for i := range vv { + vv[i] = nil + } + vm.sp = sp + ss + vm.pc++ +} + +// Similar to enterFunc, but for when arguments may be accessed before they are initialised, +// e.g. by an eval() code or from a closure, or from an earlier initialiser code. +// In this case the arguments remain on stack, first argsToCopy of them are copied to the stash. +type enterFunc1 struct { + names map[unistring.String]uint32 + stashSize uint32 + numArgs uint32 + argsToCopy uint32 + funcType funcType + extensible bool +} + +func (e *enterFunc1) exec(vm *vm) { + sp := vm.sp + vm.sb = sp - vm.args - 1 + vm.newStash() + stash := vm.stash + stash.funcType = e.funcType + stash.values = make([]Value, e.stashSize) + if len(e.names) > 0 { + if e.extensible { + m := make(map[unistring.String]uint32, len(e.names)) + for name, idx := range e.names { + m[name] = idx + } + stash.names = m + } else { + stash.names = e.names + } + } + offset := vm.args - int(e.argsToCopy) + if offset > 0 { + copy(stash.values, vm.stack[sp-vm.args:sp-offset]) + if offset := vm.args - int(e.numArgs); offset > 0 { + vm.stash.extraArgs = make([]Value, offset) + copy(stash.extraArgs, vm.stack[sp-offset:]) + } + } else { + copy(stash.values, vm.stack[sp-vm.args:sp]) + if int(e.argsToCopy) > vm.args { + vv := stash.values[vm.args:e.argsToCopy] + for i := range vv { + vv[i] = _undefined + } + } + } + + vm.pc++ +} + +// Finalises the initialisers section and starts the function body which has its own +// scope. When used in conjunction with enterFunc1 adjustStack is set to true which +// causes the arguments to be removed from the stack. +type enterFuncBody struct { + enterBlock + funcType funcType + extensible bool + adjustStack bool +} + +func (e *enterFuncBody) exec(vm *vm) { + if e.stashSize > 0 || e.extensible { + vm.newStash() + stash := vm.stash + stash.funcType = e.funcType + stash.values = make([]Value, e.stashSize) + if len(e.names) > 0 { + if e.extensible { + m := make(map[unistring.String]uint32, len(e.names)) + for name, idx := range e.names { + m[name] = idx + } + stash.names = m + } else { + stash.names = e.names + } + } + } + sp := vm.sp + if e.adjustStack { + sp -= vm.args + } + nsp := sp + int(e.stackSize) + if e.stackSize > 0 { + vm.stack.expand(nsp - 1) + vv := vm.stack[sp:nsp] + for i := range vv { + vv[i] = nil + } + } + vm.sp = nsp + vm.pc++ +} + +type _ret struct{} + +var ret _ret + +func (_ret) exec(vm *vm) { + // callee -3 + // this -2 <- sb + // retval -1 + + vm.stack[vm.sb-1] = vm.stack[vm.sp-1] + vm.sp = vm.sb + vm.popCtx() + vm.pc++ +} + +type cret uint32 + +func (c cret) exec(vm *vm) { + vm.stack[vm.sb] = *vm.getStashPtr(uint32(c)) + ret.exec(vm) +} + +type enterFuncStashless struct { + stackSize uint32 + args uint32 +} + +func (e *enterFuncStashless) exec(vm *vm) { + sp := vm.sp + vm.sb = sp - vm.args - 1 + d := int(e.args) - vm.args + if d > 0 { + ss := sp + int(e.stackSize) + d + vm.stack.expand(ss) + vv := vm.stack[sp : sp+d] + for i := range vv { + vv[i] = _undefined + } + vv = vm.stack[sp+d : ss] + for i := range vv { + vv[i] = nil + } + vm.args = int(e.args) + vm.sp = ss + } else { + if e.stackSize > 0 { + ss := sp + int(e.stackSize) + vm.stack.expand(ss) + vv := vm.stack[sp:ss] + for i := range vv { + vv[i] = nil + } + vm.sp = ss + } + } + vm.pc++ +} + +type newFuncInstruction interface { + getPrg() *Program +} + +type newFunc struct { + prg *Program + name unistring.String + source string + + length int + strict bool +} + +func (n *newFunc) exec(vm *vm) { + obj := vm.r.newFunc(n.name, n.length, n.strict) + obj.prg = n.prg + obj.stash = vm.stash + obj.privEnv = vm.privEnv + obj.src = n.source + vm.push(obj.val) + vm.pc++ +} + +func (n *newFunc) getPrg() *Program { + return n.prg +} + +type newAsyncFunc struct { + newFunc +} + +func (n *newAsyncFunc) exec(vm *vm) { + obj := vm.r.newAsyncFunc(n.name, n.length, n.strict) + obj.prg = n.prg + obj.stash = vm.stash + obj.privEnv = vm.privEnv + obj.src = n.source + vm.push(obj.val) + vm.pc++ +} + +type newGeneratorFunc struct { + newFunc +} + +func (n *newGeneratorFunc) exec(vm *vm) { + obj := vm.r.newGeneratorFunc(n.name, n.length, n.strict) + obj.prg = n.prg + obj.stash = vm.stash + obj.privEnv = vm.privEnv + obj.src = n.source + vm.push(obj.val) + vm.pc++ +} + +type newMethod struct { + newFunc + homeObjOffset uint32 +} + +func (n *newMethod) _exec(vm *vm, obj *methodFuncObject) { + obj.prg = n.prg + obj.stash = vm.stash + obj.privEnv = vm.privEnv + obj.src = n.source + if n.homeObjOffset > 0 { + obj.homeObject = vm.r.toObject(vm.stack[vm.sp-int(n.homeObjOffset)]) + } + vm.push(obj.val) + vm.pc++ +} + +func (n *newMethod) exec(vm *vm) { + n._exec(vm, vm.r.newMethod(n.name, n.length, n.strict)) +} + +type newAsyncMethod struct { + newMethod +} + +func (n *newAsyncMethod) exec(vm *vm) { + obj := vm.r.newAsyncMethod(n.name, n.length, n.strict) + n._exec(vm, &obj.methodFuncObject) +} + +type newGeneratorMethod struct { + newMethod +} + +func (n *newGeneratorMethod) exec(vm *vm) { + obj := vm.r.newGeneratorMethod(n.name, n.length, n.strict) + n._exec(vm, &obj.methodFuncObject) +} + +type newArrowFunc struct { + newFunc +} + +type newAsyncArrowFunc struct { + newArrowFunc +} + +func getFuncObject(v Value) *Object { + if o, ok := v.(*Object); ok { + if fn, ok := o.self.(*arrowFuncObject); ok { + return fn.funcObj + } + return o + } + if v == _undefined { + return nil + } + panic(typeError("Value is not an Object")) +} + +func getHomeObject(v Value) *Object { + if o, ok := v.(*Object); ok { + switch fn := o.self.(type) { + case *methodFuncObject: + return fn.homeObject + case *generatorMethodFuncObject: + return fn.homeObject + case *asyncMethodFuncObject: + return fn.homeObject + case *classFuncObject: + return o.runtime.toObject(fn.getStr("prototype", nil)) + case *arrowFuncObject: + return getHomeObject(fn.funcObj) + case *asyncArrowFuncObject: + return getHomeObject(fn.funcObj) + } + } + panic(newTypeError("Compiler bug: getHomeObject() on the wrong value: %T", v)) +} + +func (n *newArrowFunc) _exec(vm *vm, obj *arrowFuncObject) { + obj.prg = n.prg + obj.stash = vm.stash + obj.privEnv = vm.privEnv + obj.src = n.source + if vm.sb > 0 { + obj.funcObj = getFuncObject(vm.stack[vm.sb-1]) + } + vm.push(obj.val) + vm.pc++ +} + +func (n *newArrowFunc) exec(vm *vm) { + n._exec(vm, vm.r.newArrowFunc(n.name, n.length, n.strict)) +} + +func (n *newAsyncArrowFunc) exec(vm *vm) { + obj := vm.r.newAsyncArrowFunc(n.name, n.length, n.strict) + n._exec(vm, &obj.arrowFuncObject) +} + +func (vm *vm) alreadyDeclared(name unistring.String) Value { + return vm.r.newError(vm.r.getSyntaxError(), "Identifier '%s' has already been declared", name) +} + +func (vm *vm) checkBindVarsGlobal(names []unistring.String) { + o := vm.r.globalObject.self + sn := vm.r.global.stash.names + if bo, ok := o.(*baseObject); ok { + // shortcut + if bo.extensible { + for _, name := range names { + if _, exists := sn[name]; exists { + panic(vm.alreadyDeclared(name)) + } + } + } else { + for _, name := range names { + if !bo.hasOwnPropertyStr(name) { + panic(vm.r.NewTypeError("Cannot define global variable '%s', global object is not extensible", name)) + } + if _, exists := sn[name]; exists { + panic(vm.alreadyDeclared(name)) + } + } + } + } else { + for _, name := range names { + if !o.hasOwnPropertyStr(name) && !o.isExtensible() { + panic(vm.r.NewTypeError("Cannot define global variable '%s', global object is not extensible", name)) + } + if _, exists := sn[name]; exists { + panic(vm.alreadyDeclared(name)) + } + } + } +} + +func (vm *vm) createGlobalVarBindings(names []unistring.String, d bool) { + o := vm.r.globalObject.self + if bo, ok := o.(*templatedObject); ok { + for _, name := range names { + if !bo.hasOwnPropertyStr(name) && bo.extensible { + bo._putProp(name, _undefined, true, true, d) + } + } + } else { + var cf Flag + if d { + cf = FLAG_TRUE + } else { + cf = FLAG_FALSE + } + for _, name := range names { + if !o.hasOwnPropertyStr(name) && o.isExtensible() { + o.defineOwnPropertyStr(name, PropertyDescriptor{ + Value: _undefined, + Writable: FLAG_TRUE, + Enumerable: FLAG_TRUE, + Configurable: cf, + }, true) + o.setOwnStr(name, _undefined, false) + } + } + } +} + +func (vm *vm) createGlobalFuncBindings(names []unistring.String, d bool) { + o := vm.r.globalObject.self + b := vm.sp - len(names) + var shortcutObj *templatedObject + if o, ok := o.(*templatedObject); ok { + shortcutObj = o + } + for i, name := range names { + var desc PropertyDescriptor + prop := o.getOwnPropStr(name) + desc.Value = vm.stack[b+i] + if shortcutObj != nil && prop == nil && shortcutObj.extensible { + shortcutObj._putProp(name, desc.Value, true, true, d) + } else { + if prop, ok := prop.(*valueProperty); ok && !prop.configurable { + // no-op + } else { + desc.Writable = FLAG_TRUE + desc.Enumerable = FLAG_TRUE + if d { + desc.Configurable = FLAG_TRUE + } else { + desc.Configurable = FLAG_FALSE + } + } + if shortcutObj != nil { + shortcutObj.defineOwnPropertyStr(name, desc, true) + } else { + o.defineOwnPropertyStr(name, desc, true) + o.setOwnStr(name, desc.Value, false) // not a bug, see https://262.ecma-international.org/#sec-createglobalfunctionbinding + } + } + } + vm.sp = b +} + +func (vm *vm) checkBindFuncsGlobal(names []unistring.String) { + o := vm.r.globalObject.self + sn := vm.r.global.stash.names + for _, name := range names { + if _, exists := sn[name]; exists { + panic(vm.alreadyDeclared(name)) + } + prop := o.getOwnPropStr(name) + allowed := true + switch prop := prop.(type) { + case nil: + allowed = o.isExtensible() + case *valueProperty: + allowed = prop.configurable || prop.getterFunc == nil && prop.setterFunc == nil && prop.writable && prop.enumerable + } + if !allowed { + panic(vm.r.NewTypeError("Cannot redefine global function '%s'", name)) + } + } +} + +func (vm *vm) checkBindLexGlobal(names []unistring.String) { + o := vm.r.globalObject.self + s := &vm.r.global.stash + for _, name := range names { + if _, exists := s.names[name]; exists { + goto fail + } + if prop, ok := o.getOwnPropStr(name).(*valueProperty); ok && !prop.configurable { + goto fail + } + continue + fail: + panic(vm.alreadyDeclared(name)) + } +} + +type bindVars struct { + names []unistring.String + deletable bool +} + +func (d *bindVars) exec(vm *vm) { + var target *stash + for _, name := range d.names { + for s := vm.stash; s != nil; s = s.outer { + if idx, exists := s.names[name]; exists && idx&maskVar == 0 { + vm.throw(vm.alreadyDeclared(name)) + return + } + if s.isVariable() { + target = s + break + } + } + } + if target == nil { + target = vm.stash + } + deletable := d.deletable + for _, name := range d.names { + target.createBinding(name, deletable) + } + vm.pc++ +} + +type bindGlobal struct { + vars, funcs, lets, consts []unistring.String + + deletable bool +} + +func (b *bindGlobal) exec(vm *vm) { + vm.checkBindFuncsGlobal(b.funcs) + vm.checkBindLexGlobal(b.lets) + vm.checkBindLexGlobal(b.consts) + vm.checkBindVarsGlobal(b.vars) + + s := &vm.r.global.stash + for _, name := range b.lets { + s.createLexBinding(name, false) + } + for _, name := range b.consts { + s.createLexBinding(name, true) + } + vm.createGlobalFuncBindings(b.funcs, b.deletable) + vm.createGlobalVarBindings(b.vars, b.deletable) + vm.pc++ +} + +type jne int32 + +func (j jne) exec(vm *vm) { + vm.sp-- + if !vm.stack[vm.sp].ToBoolean() { + vm.pc += int(j) + } else { + vm.pc++ + } +} + +type jeq int32 + +func (j jeq) exec(vm *vm) { + vm.sp-- + if vm.stack[vm.sp].ToBoolean() { + vm.pc += int(j) + } else { + vm.pc++ + } +} + +type jeq1 int32 + +func (j jeq1) exec(vm *vm) { + if vm.stack[vm.sp-1].ToBoolean() { + vm.pc += int(j) + } else { + vm.sp-- + vm.pc++ + } +} + +type jneq1 int32 + +func (j jneq1) exec(vm *vm) { + if !vm.stack[vm.sp-1].ToBoolean() { + vm.pc += int(j) + } else { + vm.sp-- + vm.pc++ + } +} + +type jdef int32 + +func (j jdef) exec(vm *vm) { + if vm.stack[vm.sp-1] != _undefined { + vm.pc += int(j) + } else { + vm.sp-- + vm.pc++ + } +} + +type jdefP int32 + +func (j jdefP) exec(vm *vm) { + if vm.stack[vm.sp-1] != _undefined { + vm.pc += int(j) + } else { + vm.pc++ + } + vm.sp-- +} + +type jopt int32 + +func (j jopt) exec(vm *vm) { + switch vm.stack[vm.sp-1] { + case _null: + vm.stack[vm.sp-1] = _undefined + fallthrough + case _undefined: + vm.pc += int(j) + default: + vm.pc++ + } +} + +type joptc int32 + +func (j joptc) exec(vm *vm) { + switch vm.stack[vm.sp-1].(type) { + case valueNull, valueUndefined, memberUnresolved: + vm.sp-- + vm.stack[vm.sp-1] = _undefined + vm.pc += int(j) + default: + vm.pc++ + } +} + +type jcoalesc int32 + +func (j jcoalesc) exec(vm *vm) { + switch vm.stack[vm.sp-1] { + case _undefined, _null: + vm.sp-- + vm.pc++ + default: + vm.pc += int(j) + } +} + +type _not struct{} + +var not _not + +func (_not) exec(vm *vm) { + if vm.stack[vm.sp-1].ToBoolean() { + vm.stack[vm.sp-1] = valueFalse + } else { + vm.stack[vm.sp-1] = valueTrue + } + vm.pc++ +} + +func toPrimitiveNumber(v Value) Value { + if o, ok := v.(*Object); ok { + return o.toPrimitiveNumber() + } + return v +} + +func toPrimitive(v Value) Value { + if o, ok := v.(*Object); ok { + return o.toPrimitive() + } + return v +} + +func cmp(px, py Value) Value { + var ret bool + xs, isPxString := px.(String) + ys, isPyString := py.(String) + + if isPxString && isPyString { + ret = xs.CompareTo(ys) < 0 + goto end + } else { + if px, ok := px.(*valueBigInt); ok && isPyString { + ny, err := stringToBigInt(ys.toTrimmedUTF8()) + if err != nil { + return _undefined + } + ret = (*big.Int)(px).Cmp(ny) < 0 + goto end + } + if py, ok := py.(*valueBigInt); ok && isPxString { + nx, err := stringToBigInt(xs.toTrimmedUTF8()) + if err != nil { + return _undefined + } + ret = nx.Cmp((*big.Int)(py)) < 0 + goto end + } + } + + px = toNumeric(px) + py = toNumeric(py) + + switch nx := px.(type) { + case valueInt: + switch ny := py.(type) { + case valueInt: + ret = nx < ny + goto end + case *valueBigInt: + ret = big.NewInt(int64(nx)).Cmp((*big.Int)(ny)) < 0 + goto end + } + case valueFloat: + switch ny := py.(type) { + case *valueBigInt: + switch { + case math.IsNaN(float64(nx)): + return _undefined + case nx == _negativeInf: + ret = true + goto end + } + if nx := big.NewFloat(float64(nx)); nx.IsInt() { + nx, _ := nx.Int(nil) + ret = nx.Cmp((*big.Int)(ny)) < 0 + } else { + ret = nx.Cmp(new(big.Float).SetInt((*big.Int)(ny))) < 0 + } + goto end + } + case *valueBigInt: + switch ny := py.(type) { + case valueInt: + ret = (*big.Int)(nx).Cmp(big.NewInt(int64(ny))) < 0 + goto end + case valueFloat: + switch { + case math.IsNaN(float64(ny)): + return _undefined + case ny == _positiveInf: + ret = true + goto end + } + if ny := big.NewFloat(float64(ny)); ny.IsInt() { + ny, _ := ny.Int(nil) + ret = (*big.Int)(nx).Cmp(ny) < 0 + } else { + ret = new(big.Float).SetInt((*big.Int)(nx)).Cmp(ny) < 0 + } + goto end + case *valueBigInt: + ret = (*big.Int)(nx).Cmp((*big.Int)(ny)) < 0 + goto end + } + } + + if nx, ny := px.ToFloat(), py.ToFloat(); math.IsNaN(nx) || math.IsNaN(ny) { + return _undefined + } else { + ret = nx < ny + } + +end: + if ret { + return valueTrue + } + return valueFalse + +} + +type _op_lt struct{} + +var op_lt _op_lt + +func (_op_lt) exec(vm *vm) { + left := toPrimitiveNumber(vm.stack[vm.sp-2]) + right := toPrimitiveNumber(vm.stack[vm.sp-1]) + + r := cmp(left, right) + if r == _undefined { + vm.stack[vm.sp-2] = valueFalse + } else { + vm.stack[vm.sp-2] = r + } + vm.sp-- + vm.pc++ +} + +type _op_lte struct{} + +var op_lte _op_lte + +func (_op_lte) exec(vm *vm) { + left := toPrimitiveNumber(vm.stack[vm.sp-2]) + right := toPrimitiveNumber(vm.stack[vm.sp-1]) + + r := cmp(right, left) + if r == _undefined || r == valueTrue { + vm.stack[vm.sp-2] = valueFalse + } else { + vm.stack[vm.sp-2] = valueTrue + } + + vm.sp-- + vm.pc++ +} + +type _op_gt struct{} + +var op_gt _op_gt + +func (_op_gt) exec(vm *vm) { + left := toPrimitiveNumber(vm.stack[vm.sp-2]) + right := toPrimitiveNumber(vm.stack[vm.sp-1]) + + r := cmp(right, left) + if r == _undefined { + vm.stack[vm.sp-2] = valueFalse + } else { + vm.stack[vm.sp-2] = r + } + vm.sp-- + vm.pc++ +} + +type _op_gte struct{} + +var op_gte _op_gte + +func (_op_gte) exec(vm *vm) { + left := toPrimitiveNumber(vm.stack[vm.sp-2]) + right := toPrimitiveNumber(vm.stack[vm.sp-1]) + + r := cmp(left, right) + if r == _undefined || r == valueTrue { + vm.stack[vm.sp-2] = valueFalse + } else { + vm.stack[vm.sp-2] = valueTrue + } + + vm.sp-- + vm.pc++ +} + +type _op_eq struct{} + +var op_eq _op_eq + +func (_op_eq) exec(vm *vm) { + if vm.stack[vm.sp-2].Equals(vm.stack[vm.sp-1]) { + vm.stack[vm.sp-2] = valueTrue + } else { + vm.stack[vm.sp-2] = valueFalse + } + vm.sp-- + vm.pc++ +} + +type _op_neq struct{} + +var op_neq _op_neq + +func (_op_neq) exec(vm *vm) { + if vm.stack[vm.sp-2].Equals(vm.stack[vm.sp-1]) { + vm.stack[vm.sp-2] = valueFalse + } else { + vm.stack[vm.sp-2] = valueTrue + } + vm.sp-- + vm.pc++ +} + +type _op_strict_eq struct{} + +var op_strict_eq _op_strict_eq + +func (_op_strict_eq) exec(vm *vm) { + if vm.stack[vm.sp-2].StrictEquals(vm.stack[vm.sp-1]) { + vm.stack[vm.sp-2] = valueTrue + } else { + vm.stack[vm.sp-2] = valueFalse + } + vm.sp-- + vm.pc++ +} + +type _op_strict_neq struct{} + +var op_strict_neq _op_strict_neq + +func (_op_strict_neq) exec(vm *vm) { + if vm.stack[vm.sp-2].StrictEquals(vm.stack[vm.sp-1]) { + vm.stack[vm.sp-2] = valueFalse + } else { + vm.stack[vm.sp-2] = valueTrue + } + vm.sp-- + vm.pc++ +} + +type _op_instanceof struct{} + +var op_instanceof _op_instanceof + +func (_op_instanceof) exec(vm *vm) { + left := vm.stack[vm.sp-2] + right := vm.r.toObject(vm.stack[vm.sp-1]) + + if instanceOfOperator(left, right) { + vm.stack[vm.sp-2] = valueTrue + } else { + vm.stack[vm.sp-2] = valueFalse + } + + vm.sp-- + vm.pc++ +} + +type _op_in struct{} + +var op_in _op_in + +func (_op_in) exec(vm *vm) { + left := vm.stack[vm.sp-2] + right := vm.r.toObject(vm.stack[vm.sp-1]) + + if right.hasProperty(left) { + vm.stack[vm.sp-2] = valueTrue + } else { + vm.stack[vm.sp-2] = valueFalse + } + + vm.sp-- + vm.pc++ +} + +type try struct { + catchOffset int32 + finallyOffset int32 +} + +func (t try) exec(vm *vm) { + var catchPos, finallyPos int32 + if t.catchOffset > 0 { + catchPos = int32(vm.pc) + t.catchOffset + } else { + catchPos = -1 + } + if t.finallyOffset > 0 { + finallyPos = int32(vm.pc) + t.finallyOffset + } else { + finallyPos = -1 + } + vm.pushTryFrame(catchPos, finallyPos) + vm.pc++ +} + +type leaveTry struct{} + +func (leaveTry) exec(vm *vm) { + tf := &vm.tryStack[len(vm.tryStack)-1] + if tf.finallyPos >= 0 { + tf.finallyRet = int32(vm.pc + 1) + vm.pc = int(tf.finallyPos) + tf.finallyPos = -1 + tf.catchPos = -1 + } else { + vm.popTryFrame() + vm.pc++ + } +} + +type enterFinally struct{} + +func (enterFinally) exec(vm *vm) { + tf := &vm.tryStack[len(vm.tryStack)-1] + tf.finallyPos = -1 + vm.pc++ +} + +type leaveFinally struct{} + +func (leaveFinally) exec(vm *vm) { + tf := &vm.tryStack[len(vm.tryStack)-1] + ex, ret := tf.exception, tf.finallyRet + tf.exception = nil + vm.popTryFrame() + if ex != nil { + vm.throw(ex) + return + } else { + if ret != -1 { + vm.pc = int(ret) + } else { + vm.pc++ + } + } +} + +type _throw struct{} + +var throw _throw + +func (_throw) exec(vm *vm) { + v := vm.stack[vm.sp-1] + ex := &Exception{ + val: v, + } + + if o, ok := v.(*Object); ok { + if e, ok := o.self.(*errorObject); ok { + if len(e.stack) > 0 { + ex.stack = e.stack + } + } + } + + if ex.stack == nil { + ex.stack = vm.captureStack(make([]StackFrame, 0, len(vm.callStack)+1), 0) + } + + if ex = vm.handleThrow(ex); ex != nil { + panic(ex) + } +} + +type _newVariadic struct{} + +var newVariadic _newVariadic + +func (_newVariadic) exec(vm *vm) { + _new(vm.countVariadicArgs() - 1).exec(vm) +} + +type _new uint32 + +func (n _new) exec(vm *vm) { + sp := vm.sp - int(n) + obj := vm.stack[sp-1] + ctor := vm.r.toConstructor(obj) + vm.stack[sp-1] = ctor(vm.stack[sp:vm.sp], nil) + vm.sp = sp + vm.pc++ +} + +type superCall uint32 + +func (s superCall) exec(vm *vm) { + l := len(vm.refStack) - 1 + thisRef := vm.refStack[l] + vm.refStack[l] = nil + vm.refStack = vm.refStack[:l] + + obj := vm.r.toObject(vm.stack[vm.sb-1]) + var cls *classFuncObject + switch fn := obj.self.(type) { + case *classFuncObject: + cls = fn + case *arrowFuncObject: + cls, _ = fn.funcObj.self.(*classFuncObject) + } + if cls == nil { + vm.throw(vm.r.NewTypeError("wrong callee type for super()")) + return + } + sp := vm.sp - int(s) + newTarget := vm.r.toObject(vm.newTarget) + v := cls.createInstance(vm.stack[sp:vm.sp], newTarget) + thisRef.set(v) + vm.sp = sp + cls._initFields(v) + vm.push(v) + vm.pc++ +} + +type _superCallVariadic struct{} + +var superCallVariadic _superCallVariadic + +func (_superCallVariadic) exec(vm *vm) { + superCall(vm.countVariadicArgs()).exec(vm) +} + +type _loadNewTarget struct{} + +var loadNewTarget _loadNewTarget + +func (_loadNewTarget) exec(vm *vm) { + if t := vm.newTarget; t != nil { + vm.push(t) + } else { + vm.push(_undefined) + } + vm.pc++ +} + +type _typeof struct{} + +var typeof _typeof + +func (_typeof) exec(vm *vm) { + var r Value + switch v := vm.stack[vm.sp-1].(type) { + case valueUndefined, valueUnresolved: + r = stringUndefined + case valueNull: + r = stringObjectC + case *Object: + r = v.self.typeOf() + case valueBool: + r = stringBoolean + case String: + r = stringString + case valueInt, valueFloat: + r = stringNumber + case *valueBigInt: + r = stringBigInt + case *Symbol: + r = stringSymbol + default: + panic(newTypeError("Compiler bug: unknown type: %T", v)) + } + vm.stack[vm.sp-1] = r + vm.pc++ +} + +type createArgsMapped uint32 + +func (formalArgs createArgsMapped) exec(vm *vm) { + v := &Object{runtime: vm.r} + args := &argumentsObject{} + args.extensible = true + args.prototype = vm.r.global.ObjectPrototype + args.class = "Arguments" + v.self = args + args.val = v + args.length = vm.args + args.init() + i := 0 + c := int(formalArgs) + if vm.args < c { + c = vm.args + } + for ; i < c; i++ { + args._put(unistring.String(strconv.Itoa(i)), &mappedProperty{ + valueProperty: valueProperty{ + writable: true, + configurable: true, + enumerable: true, + }, + v: &vm.stash.values[i], + }) + } + + for _, v := range vm.stash.extraArgs { + args._put(unistring.String(strconv.Itoa(i)), v) + i++ + } + + args._putProp("callee", vm.stack[vm.sb-1], true, false, true) + args._putSym(SymIterator, valueProp(vm.r.getArrayValues(), true, false, true)) + vm.push(v) + vm.pc++ +} + +type createArgsUnmapped uint32 + +func (formalArgs createArgsUnmapped) exec(vm *vm) { + args := vm.r.newBaseObject(vm.r.global.ObjectPrototype, "Arguments") + i := 0 + c := int(formalArgs) + if vm.args < c { + c = vm.args + } + for _, v := range vm.stash.values[:c] { + args._put(unistring.String(strconv.Itoa(i)), v) + i++ + } + + for _, v := range vm.stash.extraArgs { + args._put(unistring.String(strconv.Itoa(i)), v) + i++ + } + + args._putProp("length", intToValue(int64(vm.args)), true, false, true) + args._put("callee", vm.r.newThrowerProperty(false)) + args._putSym(SymIterator, valueProp(vm.r.getArrayValues(), true, false, true)) + vm.push(args.val) + vm.pc++ +} + +type _enterWith struct{} + +var enterWith _enterWith + +func (_enterWith) exec(vm *vm) { + vm.newStash() + vm.stash.obj = vm.stack[vm.sp-1].ToObject(vm.r) + vm.sp-- + vm.pc++ +} + +type _leaveWith struct{} + +var leaveWith _leaveWith + +func (_leaveWith) exec(vm *vm) { + vm.stash = vm.stash.outer + vm.pc++ +} + +func emptyIter() (propIterItem, iterNextFunc) { + return propIterItem{}, nil +} + +type _enumerate struct{} + +var enumerate _enumerate + +func (_enumerate) exec(vm *vm) { + v := vm.stack[vm.sp-1] + if v == _undefined || v == _null { + vm.iterStack = append(vm.iterStack, iterStackItem{f: emptyIter}) + } else { + vm.iterStack = append(vm.iterStack, iterStackItem{f: enumerateRecursive(v.ToObject(vm.r))}) + } + vm.sp-- + vm.pc++ +} + +type enumNext int32 + +func (jmp enumNext) exec(vm *vm) { + l := len(vm.iterStack) - 1 + item, n := vm.iterStack[l].f() + if n != nil { + vm.iterStack[l].val = item.name + vm.iterStack[l].f = n + vm.pc++ + } else { + vm.pc += int(jmp) + } +} + +type _enumGet struct{} + +var enumGet _enumGet + +func (_enumGet) exec(vm *vm) { + l := len(vm.iterStack) - 1 + vm.push(vm.iterStack[l].val) + vm.pc++ +} + +type _enumPop struct{} + +var enumPop _enumPop + +func (_enumPop) exec(vm *vm) { + l := len(vm.iterStack) - 1 + vm.iterStack[l] = iterStackItem{} + vm.iterStack = vm.iterStack[:l] + vm.pc++ +} + +type _enumPopClose struct{} + +var enumPopClose _enumPopClose + +func (_enumPopClose) exec(vm *vm) { + l := len(vm.iterStack) - 1 + item := vm.iterStack[l] + vm.iterStack[l] = iterStackItem{} + vm.iterStack = vm.iterStack[:l] + if iter := item.iter; iter != nil { + iter.returnIter() + } + vm.pc++ +} + +type _iterateP struct{} + +var iterateP _iterateP + +func (_iterateP) exec(vm *vm) { + iter := vm.r.getIterator(vm.stack[vm.sp-1], nil) + vm.iterStack = append(vm.iterStack, iterStackItem{iter: iter}) + vm.sp-- + vm.pc++ +} + +type _iterate struct{} + +var iterate _iterate + +func (_iterate) exec(vm *vm) { + iter := vm.r.getIterator(vm.stack[vm.sp-1], nil) + vm.iterStack = append(vm.iterStack, iterStackItem{iter: iter}) + vm.pc++ +} + +type iterNext int32 + +func (jmp iterNext) exec(vm *vm) { + l := len(vm.iterStack) - 1 + iter := vm.iterStack[l].iter + value, ex := iter.step() + if ex == nil { + if value == nil { + vm.pc += int(jmp) + } else { + vm.iterStack[l].val = value + vm.pc++ + } + } else { + l := len(vm.iterStack) - 1 + vm.iterStack[l] = iterStackItem{} + vm.iterStack = vm.iterStack[:l] + vm.throw(ex.val) + return + } +} + +type iterGetNextOrUndef struct{} + +func (iterGetNextOrUndef) exec(vm *vm) { + l := len(vm.iterStack) - 1 + iter := vm.iterStack[l].iter + var value Value + if iter.iterator != nil { + var ex *Exception + value, ex = iter.step() + if ex != nil { + l := len(vm.iterStack) - 1 + vm.iterStack[l] = iterStackItem{} + vm.iterStack = vm.iterStack[:l] + vm.throw(ex.val) + return + } + } + vm.push(nilSafe(value)) + vm.pc++ +} + +type copyStash struct{} + +func (copyStash) exec(vm *vm) { + oldStash := vm.stash + newStash := &stash{ + outer: oldStash.outer, + } + vm.stashAllocs++ + newStash.values = append([]Value(nil), oldStash.values...) + newStash.names = oldStash.names + vm.stash = newStash + vm.pc++ +} + +type _throwAssignToConst struct{} + +var throwAssignToConst _throwAssignToConst + +func (_throwAssignToConst) exec(vm *vm) { + vm.throw(errAssignToConst) +} + +func (r *Runtime) copyDataProperties(target, source Value) { + targetObj := r.toObject(target) + if source == _null || source == _undefined { + return + } + sourceObj := source.ToObject(r) + for item, next := iterateEnumerableProperties(sourceObj)(); next != nil; item, next = next() { + createDataPropertyOrThrow(targetObj, item.name, item.value) + } +} + +type _copySpread struct{} + +var copySpread _copySpread + +func (_copySpread) exec(vm *vm) { + vm.r.copyDataProperties(vm.stack[vm.sp-2], vm.stack[vm.sp-1]) + vm.sp-- + vm.pc++ +} + +type _copyRest struct{} + +var copyRest _copyRest + +func (_copyRest) exec(vm *vm) { + vm.push(vm.r.NewObject()) + vm.r.copyDataProperties(vm.stack[vm.sp-1], vm.stack[vm.sp-2]) + vm.pc++ +} + +type _createDestructSrc struct{} + +var createDestructSrc _createDestructSrc + +func (_createDestructSrc) exec(vm *vm) { + v := vm.stack[vm.sp-1] + vm.r.checkObjectCoercible(v) + vm.push(vm.r.newDestructKeyedSource(v)) + vm.pc++ +} + +type _checkObjectCoercible struct{} + +var checkObjectCoercible _checkObjectCoercible + +func (_checkObjectCoercible) exec(vm *vm) { + vm.r.checkObjectCoercible(vm.stack[vm.sp-1]) + vm.pc++ +} + +type createArgsRestStack int + +func (n createArgsRestStack) exec(vm *vm) { + var values []Value + delta := vm.args - int(n) + if delta > 0 { + values = make([]Value, delta) + copy(values, vm.stack[vm.sb+int(n)+1:]) + } + vm.push(vm.r.newArrayValues(values)) + vm.pc++ +} + +type _createArgsRestStash struct{} + +var createArgsRestStash _createArgsRestStash + +func (_createArgsRestStash) exec(vm *vm) { + vm.push(vm.r.newArrayValues(vm.stash.extraArgs)) + vm.stash.extraArgs = nil + vm.pc++ +} + +type concatStrings int + +func (n concatStrings) exec(vm *vm) { + strs := vm.stack[vm.sp-int(n) : vm.sp] + length := 0 + allAscii := true + for i, s := range strs { + switch s := s.(type) { + case asciiString: + length += s.Length() + case unicodeString: + length += s.Length() + allAscii = false + case *importedString: + s.ensureScanned() + if s.u != nil { + strs[i] = s.u + length += s.u.Length() + allAscii = false + } else { + strs[i] = asciiString(s.s) + length += len(s.s) + } + default: + panic(unknownStringTypeErr(s)) + } + } + + vm.sp -= int(n) - 1 + if allAscii { + var buf strings.Builder + buf.Grow(length) + for _, s := range strs { + buf.WriteString(string(s.(asciiString))) + } + vm.stack[vm.sp-1] = asciiString(buf.String()) + } else { + var buf unicodeStringBuilder + buf.ensureStarted(length) + for _, s := range strs { + buf.writeString(s.(String)) + } + vm.stack[vm.sp-1] = buf.String() + } + vm.pc++ +} + +type getTaggedTmplObject struct { + raw, cooked []Value +} + +// As tagged template objects are not cached (because it's hard to ensure the cache is cleaned without using +// finalizers) this wrapper is needed to override the equality method so that two objects for the same template +// literal appeared to be equal from the code's point of view. +type taggedTemplateArray struct { + *arrayObject + idPtr *[]Value +} + +func (a *taggedTemplateArray) equal(other objectImpl) bool { + if o, ok := other.(*taggedTemplateArray); ok { + return a.idPtr == o.idPtr + } + return false +} + +func (c *getTaggedTmplObject) exec(vm *vm) { + cooked := vm.r.newArrayObject() + setArrayValues(cooked, c.cooked) + raw := vm.r.newArrayObject() + setArrayValues(raw, c.raw) + + cooked.propValueCount = len(c.cooked) + cooked.lengthProp.writable = false + + raw.propValueCount = len(c.raw) + raw.lengthProp.writable = false + + raw.preventExtensions(true) + raw.val.self = &taggedTemplateArray{ + arrayObject: raw, + idPtr: &c.raw, + } + + cooked._putProp("raw", raw.val, false, false, false) + cooked.preventExtensions(true) + cooked.val.self = &taggedTemplateArray{ + arrayObject: cooked, + idPtr: &c.cooked, + } + + vm.push(cooked.val) + vm.pc++ +} + +type _loadSuper struct{} + +var loadSuper _loadSuper + +func (_loadSuper) exec(vm *vm) { + homeObject := getHomeObject(vm.stack[vm.sb-1]) + if proto := homeObject.Prototype(); proto != nil { + vm.push(proto) + } else { + vm.push(_undefined) + } + vm.pc++ +} + +type newClass struct { + ctor *Program + name unistring.String + source string + initFields *Program + + privateFields, privateMethods []unistring.String // only set when dynamic resolution is needed + numPrivateFields, numPrivateMethods uint32 + + length int + hasPrivateEnv bool +} + +type newDerivedClass struct { + newClass +} + +func (vm *vm) createPrivateType(f *classFuncObject, numFields, numMethods uint32) { + typ := &privateEnvType{} + typ.numFields = numFields + typ.numMethods = numMethods + f.privateEnvType = typ + f.privateMethods = make([]Value, numMethods) +} + +func (vm *vm) fillPrivateNamesMap(typ *privateEnvType, privateFields, privateMethods []unistring.String) { + if len(privateFields) > 0 || len(privateMethods) > 0 { + penv := vm.privEnv.names + if penv == nil { + penv = make(privateNames) + vm.privEnv.names = penv + } + for idx, field := range privateFields { + penv[field] = &privateId{ + typ: typ, + idx: uint32(idx), + } + } + for idx, method := range privateMethods { + penv[method] = &privateId{ + typ: typ, + idx: uint32(idx), + isMethod: true, + } + } + } +} + +func (c *newClass) create(protoParent, ctorParent *Object, vm *vm, derived bool) (prototype, cls *Object) { + proto := vm.r.newBaseObject(protoParent, classObject) + f := vm.r.newClassFunc(c.name, c.length, ctorParent, derived) + f._putProp("prototype", proto.val, false, false, false) + proto._putProp("constructor", f.val, true, false, true) + f.prg = c.ctor + f.stash = vm.stash + f.src = c.source + f.initFields = c.initFields + if c.hasPrivateEnv { + vm.privEnv = &privateEnv{ + outer: vm.privEnv, + } + vm.createPrivateType(f, c.numPrivateFields, c.numPrivateMethods) + vm.fillPrivateNamesMap(f.privateEnvType, c.privateFields, c.privateMethods) + vm.privEnv.instanceType = f.privateEnvType + } + f.privEnv = vm.privEnv + return proto.val, f.val +} + +func (c *newClass) exec(vm *vm) { + proto, cls := c.create(vm.r.global.ObjectPrototype, vm.r.getFunctionPrototype(), vm, false) + sp := vm.sp + vm.stack.expand(sp + 1) + vm.stack[sp] = proto + vm.stack[sp+1] = cls + vm.sp = sp + 2 + vm.pc++ +} + +func (c *newDerivedClass) exec(vm *vm) { + var protoParent *Object + var superClass *Object + if o := vm.stack[vm.sp-1]; o != _null { + if sc, ok := o.(*Object); !ok || sc.self.assertConstructor() == nil { + vm.throw(vm.r.NewTypeError("Class extends value is not a constructor or null")) + return + } else { + v := sc.self.getStr("prototype", nil) + if v != _null { + if o, ok := v.(*Object); ok { + protoParent = o + } else { + vm.throw(vm.r.NewTypeError("Class extends value does not have valid prototype property")) + return + } + } + superClass = sc + } + } else { + superClass = vm.r.getFunctionPrototype() + } + + proto, cls := c.create(protoParent, superClass, vm, true) + vm.stack[vm.sp-1] = proto + vm.push(cls) + vm.pc++ +} + +// Creates a special instance of *classFuncObject which is only used during evaluation of a class declaration +// to initialise static fields and instance private methods of another class. +type newStaticFieldInit struct { + initFields *Program + numPrivateFields, numPrivateMethods uint32 +} + +func (c *newStaticFieldInit) exec(vm *vm) { + f := vm.r.newClassFunc("", 0, vm.r.getFunctionPrototype(), false) + if c.numPrivateFields > 0 || c.numPrivateMethods > 0 { + vm.createPrivateType(f, c.numPrivateFields, c.numPrivateMethods) + } + f.initFields = c.initFields + f.stash = vm.stash + vm.push(f.val) + vm.pc++ +} + +func (vm *vm) loadThis(v Value) { + if v != nil { + vm.push(v) + } else { + vm.throw(vm.r.newError(vm.r.getReferenceError(), "Must call super constructor in derived class before accessing 'this'")) + return + } + vm.pc++ +} + +type loadThisStash uint32 + +func (l loadThisStash) exec(vm *vm) { + vm.loadThis(*vm.getStashPtr(uint32(l))) +} + +type loadThisStack struct{} + +func (loadThisStack) exec(vm *vm) { + vm.loadThis(vm.stack[vm.sb]) +} + +func (vm *vm) getStashPtr(s uint32) *Value { + level := int(s) >> 24 + idx := s & 0x00FFFFFF + stash := vm.stash + for i := 0; i < level; i++ { + stash = stash.outer + } + + return &stash.values[idx] +} + +type getThisDynamic struct{} + +func (getThisDynamic) exec(vm *vm) { + for stash := vm.stash; stash != nil; stash = stash.outer { + if stash.obj == nil { + if v, exists := stash.getByName(thisBindingName); exists { + vm.push(v) + vm.pc++ + return + } + } + } + vm.push(vm.r.globalObject) + vm.pc++ +} + +type throwConst struct { + v interface{} +} + +func (t throwConst) exec(vm *vm) { + vm.throw(t.v) +} + +type resolveThisStack struct{} + +func (r resolveThisStack) exec(vm *vm) { + vm.refStack = append(vm.refStack, &thisRef{v: (*[]Value)(&vm.stack), idx: vm.sb}) + vm.pc++ +} + +type resolveThisStash uint32 + +func (r resolveThisStash) exec(vm *vm) { + level := int(r) >> 24 + idx := r & 0x00FFFFFF + stash := vm.stash + for i := 0; i < level; i++ { + stash = stash.outer + } + vm.refStack = append(vm.refStack, &thisRef{v: &stash.values, idx: int(idx)}) + vm.pc++ +} + +type resolveThisDynamic struct{} + +func (resolveThisDynamic) exec(vm *vm) { + for stash := vm.stash; stash != nil; stash = stash.outer { + if stash.obj == nil { + if idx, exists := stash.names[thisBindingName]; exists { + vm.refStack = append(vm.refStack, &thisRef{v: &stash.values, idx: int(idx &^ maskTyp)}) + vm.pc++ + return + } + } + } + panic(vm.r.newError(vm.r.getReferenceError(), "Compiler bug: 'this' reference is not found in resolveThisDynamic")) +} + +type defineComputedKey int + +func (offset defineComputedKey) exec(vm *vm) { + obj := vm.r.toObject(vm.stack[vm.sp-int(offset)]) + if h, ok := obj.self.(*classFuncObject); ok { + key := toPropertyKey(vm.stack[vm.sp-1]) + h.computedKeys = append(h.computedKeys, key) + vm.sp-- + vm.pc++ + return + } + panic(vm.r.NewTypeError("Compiler bug: unexpected target for defineComputedKey: %v", obj)) +} + +type loadComputedKey int + +func (idx loadComputedKey) exec(vm *vm) { + obj := vm.r.toObject(vm.stack[vm.sb-1]) + if h, ok := obj.self.(*classFuncObject); ok { + vm.push(h.computedKeys[idx]) + vm.pc++ + return + } + panic(vm.r.NewTypeError("Compiler bug: unexpected target for loadComputedKey: %v", obj)) +} + +type initStaticElements struct { + privateFields, privateMethods []unistring.String +} + +func (i *initStaticElements) exec(vm *vm) { + cls := vm.stack[vm.sp-1] + staticInit := vm.r.toObject(vm.stack[vm.sp-3]) + vm.sp -= 2 + if h, ok := staticInit.self.(*classFuncObject); ok { + h._putProp("prototype", cls, true, true, true) // so that 'super' resolution work + h.privEnv = vm.privEnv + if h.privateEnvType != nil { + vm.privEnv.staticType = h.privateEnvType + vm.fillPrivateNamesMap(h.privateEnvType, i.privateFields, i.privateMethods) + } + h._initFields(vm.r.toObject(cls)) + vm.stack[vm.sp-1] = cls + + vm.pc++ + return + } + panic(vm.r.NewTypeError("Compiler bug: unexpected target for initStaticElements: %v", staticInit)) +} + +type definePrivateMethod struct { + idx int + targetOffset int +} + +func (d *definePrivateMethod) getPrivateMethods(vm *vm) []Value { + obj := vm.r.toObject(vm.stack[vm.sp-d.targetOffset]) + if cls, ok := obj.self.(*classFuncObject); ok { + return cls.privateMethods + } else { + panic(vm.r.NewTypeError("Compiler bug: wrong target type for definePrivateMethod: %T", obj.self)) + } +} + +func (d *definePrivateMethod) exec(vm *vm) { + methods := d.getPrivateMethods(vm) + methods[d.idx] = vm.stack[vm.sp-1] + vm.sp-- + vm.pc++ +} + +type definePrivateGetter struct { + definePrivateMethod +} + +func (d *definePrivateGetter) exec(vm *vm) { + methods := d.getPrivateMethods(vm) + val := vm.stack[vm.sp-1] + method := vm.r.toObject(val) + p, _ := methods[d.idx].(*valueProperty) + if p == nil { + p = &valueProperty{ + accessor: true, + } + methods[d.idx] = p + } + if p.getterFunc != nil { + vm.throw(vm.r.NewTypeError("Private getter has already been declared")) + return + } + p.getterFunc = method + vm.sp-- + vm.pc++ +} + +type definePrivateSetter struct { + definePrivateMethod +} + +func (d *definePrivateSetter) exec(vm *vm) { + methods := d.getPrivateMethods(vm) + val := vm.stack[vm.sp-1] + method := vm.r.toObject(val) + p, _ := methods[d.idx].(*valueProperty) + if p == nil { + p = &valueProperty{ + accessor: true, + } + methods[d.idx] = p + } + if p.setterFunc != nil { + vm.throw(vm.r.NewTypeError("Private setter has already been declared")) + return + } + p.setterFunc = method + vm.sp-- + vm.pc++ +} + +type definePrivateProp struct { + idx int +} + +func (d *definePrivateProp) exec(vm *vm) { + f := vm.r.toObject(vm.stack[vm.sb-1]).self.(*classFuncObject) + obj := vm.r.toObject(vm.stack[vm.sp-2]) + penv := obj.self.getPrivateEnv(f.privateEnvType, false) + penv.fields[d.idx] = vm.stack[vm.sp-1] + vm.sp-- + vm.pc++ +} + +type getPrivatePropRes resolvedPrivateName + +func (vm *vm) getPrivateType(level uint8, isStatic bool) *privateEnvType { + e := vm.privEnv + for i := uint8(0); i < level; i++ { + e = e.outer + } + if isStatic { + return e.staticType + } + return e.instanceType +} + +func (g *getPrivatePropRes) _get(base Value, vm *vm) Value { + return vm.getPrivateProp(base, g.name, vm.getPrivateType(g.level, g.isStatic), g.idx, g.isMethod) +} + +func (g *getPrivatePropRes) exec(vm *vm) { + vm.stack[vm.sp-1] = g._get(vm.stack[vm.sp-1], vm) + vm.pc++ +} + +type getPrivatePropId privateId + +func (g *getPrivatePropId) exec(vm *vm) { + vm.stack[vm.sp-1] = vm.getPrivateProp(vm.stack[vm.sp-1], g.name, g.typ, g.idx, g.isMethod) + vm.pc++ +} + +type getPrivatePropIdCallee privateId + +func (g *getPrivatePropIdCallee) exec(vm *vm) { + prop := vm.getPrivateProp(vm.stack[vm.sp-1], g.name, g.typ, g.idx, g.isMethod) + if prop == nil { + prop = memberUnresolved{valueUnresolved{r: vm.r, ref: (*privateId)(g).string()}} + } + vm.push(prop) + + vm.pc++ +} + +func (vm *vm) getPrivateProp(base Value, name unistring.String, typ *privateEnvType, idx uint32, isMethod bool) Value { + obj := vm.r.toObject(base) + penv := obj.self.getPrivateEnv(typ, false) + var v Value + if penv != nil { + if isMethod { + v = penv.methods[idx] + } else { + v = penv.fields[idx] + if v == nil { + panic(vm.r.NewTypeError("Private member #%s is accessed before it is initialized", name)) + } + } + } else { + panic(vm.r.NewTypeError("Cannot read private member #%s from an object whose class did not declare it", name)) + } + if prop, ok := v.(*valueProperty); ok { + if prop.getterFunc == nil { + panic(vm.r.NewTypeError("'#%s' was defined without a getter", name)) + } + v = prop.get(obj) + } + return v +} + +type getPrivatePropResCallee getPrivatePropRes + +func (g *getPrivatePropResCallee) exec(vm *vm) { + prop := (*getPrivatePropRes)(g)._get(vm.stack[vm.sp-1], vm) + if prop == nil { + prop = memberUnresolved{valueUnresolved{r: vm.r, ref: (*resolvedPrivateName)(g).string()}} + } + vm.push(prop) + + vm.pc++ +} + +func (vm *vm) setPrivateProp(base Value, name unistring.String, typ *privateEnvType, idx uint32, isMethod bool, val Value) { + obj := vm.r.toObject(base) + penv := obj.self.getPrivateEnv(typ, false) + if penv != nil { + if isMethod { + v := penv.methods[idx] + if prop, ok := v.(*valueProperty); ok { + if prop.setterFunc != nil { + prop.set(base, val) + } else { + panic(vm.r.NewTypeError("Cannot assign to read only property '#%s'", name)) + } + } else { + panic(vm.r.NewTypeError("Private method '#%s' is not writable", name)) + } + } else { + ptr := &penv.fields[idx] + if *ptr == nil { + panic(vm.r.NewTypeError("Private member #%s is accessed before it is initialized", name)) + } + *ptr = val + } + } else { + panic(vm.r.NewTypeError("Cannot write private member #%s from an object whose class did not declare it", name)) + } +} + +func (vm *vm) exceptionFromValue(x interface{}) *Exception { + var ex *Exception + switch x1 := x.(type) { + case *Object: + ex = &Exception{ + val: x1, + } + if er, ok := x1.self.(*errorObject); ok { + ex.stack = er.stack + } + case Value: + ex = &Exception{ + val: x1, + } + case *Exception: + ex = x1 + case typeError: + ex = &Exception{ + val: vm.r.NewTypeError(string(x1)), + } + case referenceError: + ex = &Exception{ + val: vm.r.newError(vm.r.getReferenceError(), string(x1)), + } + case rangeError: + ex = &Exception{ + val: vm.r.newError(vm.r.getRangeError(), string(x1)), + } + case syntaxError: + ex = &Exception{ + val: vm.r.newError(vm.r.getSyntaxError(), string(x1)), + } + default: + /* + if vm.prg != nil { + vm.prg.dumpCode(log.Printf) + } + log.Print("Stack: ", string(debug.Stack())) + panic(fmt.Errorf("Panic at %d: %v", vm.pc, x)) + */ + return nil + } + if ex.stack == nil { + ex.stack = vm.captureStack(make([]StackFrame, 0, len(vm.callStack)+1), 0) + } + return ex +} + +type setPrivatePropRes resolvedPrivateName + +func (p *setPrivatePropRes) _set(base Value, val Value, vm *vm) { + vm.setPrivateProp(base, p.name, vm.getPrivateType(p.level, p.isStatic), p.idx, p.isMethod, val) +} + +func (p *setPrivatePropRes) exec(vm *vm) { + v := vm.stack[vm.sp-1] + p._set(vm.stack[vm.sp-2], v, vm) + vm.stack[vm.sp-2] = v + vm.sp-- + vm.pc++ +} + +type setPrivatePropResP setPrivatePropRes + +func (p *setPrivatePropResP) exec(vm *vm) { + v := vm.stack[vm.sp-1] + (*setPrivatePropRes)(p)._set(vm.stack[vm.sp-2], v, vm) + vm.sp -= 2 + vm.pc++ +} + +type setPrivatePropId privateId + +func (p *setPrivatePropId) exec(vm *vm) { + v := vm.stack[vm.sp-1] + vm.setPrivateProp(vm.stack[vm.sp-2], p.name, p.typ, p.idx, p.isMethod, v) + vm.stack[vm.sp-2] = v + vm.sp-- + vm.pc++ +} + +type setPrivatePropIdP privateId + +func (p *setPrivatePropIdP) exec(vm *vm) { + v := vm.stack[vm.sp-1] + vm.setPrivateProp(vm.stack[vm.sp-2], p.name, p.typ, p.idx, p.isMethod, v) + vm.sp -= 2 + vm.pc++ +} + +type popPrivateEnv struct{} + +func (popPrivateEnv) exec(vm *vm) { + vm.privEnv = vm.privEnv.outer + vm.pc++ +} + +type privateInRes resolvedPrivateName + +func (i *privateInRes) exec(vm *vm) { + obj := vm.r.toObject(vm.stack[vm.sp-1]) + pe := obj.self.getPrivateEnv(vm.getPrivateType(i.level, i.isStatic), false) + if pe != nil && (i.isMethod && pe.methods[i.idx] != nil || !i.isMethod && pe.fields[i.idx] != nil) { + vm.stack[vm.sp-1] = valueTrue + } else { + vm.stack[vm.sp-1] = valueFalse + } + vm.pc++ +} + +type privateInId privateId + +func (i *privateInId) exec(vm *vm) { + obj := vm.r.toObject(vm.stack[vm.sp-1]) + pe := obj.self.getPrivateEnv(i.typ, false) + if pe != nil && (i.isMethod && pe.methods[i.idx] != nil || !i.isMethod && pe.fields[i.idx] != nil) { + vm.stack[vm.sp-1] = valueTrue + } else { + vm.stack[vm.sp-1] = valueFalse + } + vm.pc++ +} + +type getPrivateRefRes resolvedPrivateName + +func (r *getPrivateRefRes) exec(vm *vm) { + vm.refStack = append(vm.refStack, &privateRefRes{ + base: vm.stack[vm.sp-1].ToObject(vm.r), + name: (*resolvedPrivateName)(r), + }) + vm.sp-- + vm.pc++ +} + +type getPrivateRefId privateId + +func (r *getPrivateRefId) exec(vm *vm) { + vm.refStack = append(vm.refStack, &privateRefId{ + base: vm.stack[vm.sp-1].ToObject(vm.r), + id: (*privateId)(r), + }) + vm.sp-- + vm.pc++ +} + +func (y *yieldMarker) exec(vm *vm) { + vm.pc = -vm.pc // this will terminate the run loop + vm.push(y) // marker so the caller knows it's a yield, not a return +} + +func (y *yieldMarker) String() string { + if y == yieldEmpty { + return "empty" + } + switch y.resultType { + case resultYield: + return "yield" + case resultYieldRes: + return "yieldRes" + case resultYieldDelegate: + return "yield*" + case resultYieldDelegateRes: + return "yield*Res" + case resultAwait: + return "await" + default: + return "unknown" + } +} diff --git a/goja/vm_test.go b/goja/vm_test.go new file mode 100644 index 0000000..a88ff47 --- /dev/null +++ b/goja/vm_test.go @@ -0,0 +1,285 @@ +package goja + +import ( + "github.com/dop251/goja/file" + "github.com/dop251/goja/parser" + "github.com/dop251/goja/unistring" + "testing" +) + +func TestTaggedTemplateArgExport(t *testing.T) { + vm := New() + vm.Set("f", func(v Value) { + v.Export() + }) + vm.RunString("f`test`") +} + +func TestVM1(t *testing.T) { + r := &Runtime{} + r.init() + + vm := r.vm + + vm.prg = &Program{ + src: file.NewFile("dummy", "", 1), + code: []instruction{ + &bindGlobal{vars: []unistring.String{"v"}}, + newObject, + setGlobal("v"), + loadVal{asciiString("test")}, + loadVal{valueInt(3)}, + loadVal{valueInt(2)}, + add, + setElem, + pop, + loadDynamic("v"), + }, + } + + vm.run() + + rv := vm.pop() + + if obj, ok := rv.(*Object); ok { + if v := obj.self.getStr("test", nil).ToInteger(); v != 5 { + t.Fatalf("Unexpected property value: %v", v) + } + } else { + t.Fatalf("Unexpected result: %v", rv) + } + +} + +func TestEvalVar(t *testing.T) { + const SCRIPT = ` + function test() { + var a; + return eval("var a = 'yes'; var z = 'no'; a;") === "yes" && a === "yes"; + } + test(); + ` + + testScript(SCRIPT, valueTrue, t) +} + +func TestResolveMixedStack1(t *testing.T) { + const SCRIPT = ` + function test(arg) { + var a = 1; + var scope = {}; + (function() {return arg})(); // move arguments to stash + with (scope) { + a++; // resolveMixedStack1 here + return a + arg; + } + } + test(40); + ` + + testScript(SCRIPT, valueInt(42), t) +} + +func TestNewArrayFromIterClosed(t *testing.T) { + const SCRIPT = ` + const [a, ...other] = []; + assert.sameValue(a, undefined); + assert(Array.isArray(other)); + assert.sameValue(other.length, 0); + ` + testScriptWithTestLib(SCRIPT, _undefined, t) +} + +func BenchmarkVmNOP2(b *testing.B) { + prg := []func(*vm){ + //loadVal(0).exec, + //loadVal(1).exec, + //add.exec, + jump(1).exec, + } + + r := &Runtime{} + r.init() + + vm := r.vm + vm.prg = &Program{} + + for i := 0; i < b.N; i++ { + vm.pc = 0 + for !vm.halted() { + prg[vm.pc](vm) + } + //vm.sp-- + /*r := vm.pop() + if r.ToInteger() != 5 { + b.Fatalf("Unexpected result: %+v", r) + } + if vm.sp != 0 { + b.Fatalf("Unexpected sp: %d", vm.sp) + }*/ + } +} + +func BenchmarkVmNOP(b *testing.B) { + r := &Runtime{} + r.init() + + vm := r.vm + vm.prg = &Program{ + code: []instruction{ + jump(1), + //jump(1), + }, + } + + for i := 0; i < b.N; i++ { + vm.pc = 0 + vm.run() + } + +} + +func BenchmarkVm1(b *testing.B) { + r := &Runtime{} + r.init() + + vm := r.vm + + //ins1 := loadVal1(0) + //ins2 := loadVal1(1) + + vm.prg = &Program{ + code: []instruction{ + loadVal{valueInt(2)}, + loadVal{valueInt(3)}, + add, + }, + } + + for i := 0; i < b.N; i++ { + vm.pc = 0 + vm.run() + r := vm.pop() + if r.ToInteger() != 5 { + b.Fatalf("Unexpected result: %+v", r) + } + if vm.sp != 0 { + b.Fatalf("Unexpected sp: %d", vm.sp) + } + } +} + +func BenchmarkFib(b *testing.B) { + const TEST_FIB = ` +function fib(n) { +if (n < 2) return n; +return fib(n - 2) + fib(n - 1); +} + +fib(35); +` + b.StopTimer() + prg, err := parser.ParseFile(nil, "test.js", TEST_FIB, 0) + if err != nil { + b.Fatal(err) + } + + c := newCompiler() + c.compile(prg, false, true, nil) + c.p.dumpCode(b.Logf) + + r := &Runtime{} + r.init() + + vm := r.vm + + var expectedResult Value = valueInt(9227465) + + b.StartTimer() + + vm.prg = c.p + vm.run() + v := vm.result + + b.Logf("stack size: %d", len(vm.stack)) + b.Logf("stashAllocs: %d", vm.stashAllocs) + + if !v.SameAs(expectedResult) { + b.Fatalf("Result: %+v, expected: %+v", v, expectedResult) + } + +} + +func BenchmarkEmptyLoop(b *testing.B) { + const SCRIPT = ` + function f() { + for (var i = 0; i < 100; i++) { + } + } + f() + ` + b.StopTimer() + vm := New() + prg := MustCompile("test.js", SCRIPT, false) + // prg.dumpCode(log.Printf) + b.StartTimer() + for i := 0; i < b.N; i++ { + vm.RunProgram(prg) + } +} + +func BenchmarkVMAdd(b *testing.B) { + vm := &vm{} + vm.stack = append(vm.stack, nil, nil) + vm.sp = len(vm.stack) + + var v1 Value = valueInt(3) + var v2 Value = valueInt(5) + + for i := 0; i < b.N; i++ { + vm.stack[0] = v1 + vm.stack[1] = v2 + add.exec(vm) + vm.sp++ + } +} + +func BenchmarkFuncCall(b *testing.B) { + const SCRIPT = ` + function f(a, b, c, d) { + } + ` + + b.StopTimer() + + vm := New() + prg := MustCompile("test.js", SCRIPT, false) + + vm.RunProgram(prg) + if f, ok := AssertFunction(vm.Get("f")); ok { + b.StartTimer() + for i := 0; i < b.N; i++ { + f(nil, nil, intToValue(1), intToValue(2), intToValue(3), intToValue(4), intToValue(5), intToValue(6)) + } + } else { + b.Fatal("f is not a function") + } +} + +func BenchmarkAssertInt(b *testing.B) { + v := intToValue(42) + for i := 0; i < b.N; i++ { + if i, ok := v.(valueInt); !ok || int64(i) != 42 { + b.Fatal() + } + } +} + +func BenchmarkLoadVal(b *testing.B) { + var ins instruction + b.ReportAllocs() + for i := 0; i < b.N; i++ { + ins = loadVal{valueInt(1)} + _ = ins + } +} diff --git a/js.go b/js.go index 5d7e869..7456a0c 100644 --- a/js.go +++ b/js.go @@ -29,15 +29,19 @@ var fileTS string //go:embed js/lib/util.ts var utilTS string +//go:embed js/lib/http.ts +var httpTS string + func RunFile(file string, args ...any) (any, error) { return Run(u.ReadFileN(file), file, args...) } func Run(code string, refFile string, args ...any) (any, error) { + rt := New() var r any - rt, err := StartFromCode(code, refFile) + _, err := rt.StartFromCode(code, refFile) if err == nil { - r, err = rt.Run(args...) + r, err = rt.RunMain(args...) } return r, err } @@ -48,11 +52,16 @@ var requireLibMatcher = regexp.MustCompile(`(?im)^\s*(const|let|var)\s+(.+?)\s*= var checkMainMatcher = regexp.MustCompile(`(?im)^\s*function\s+main\s*\(`) type Runtime struct { - vm *goja.Runtime - required map[string]bool - file string - srcCode string - code string + vm *goja.Runtime + required map[string]bool + file string + srcCode string + code string + moduleLoader func(string) string +} + +func (rt *Runtime) SetModuleLoader(fn func(filename string) string) { + rt.moduleLoader = fn } func (rt *Runtime) requireMod(name string) error { @@ -75,6 +84,12 @@ func (rt *Runtime) requireMod(name string) error { err = rt.vm.Set("util", js.RequireUtil()) } } + if err == nil && (name == "http" || name == "") { + if !rt.required["http"] { + rt.required["http"] = true + err = rt.vm.Set("http", js.RequireHTTP()) + } + } if err == nil && (name == "ai" || name == "") { if !rt.required["ai"] { rt.required["ai"] = true @@ -114,28 +129,35 @@ func (rt *Runtime) makeImport(matcher *regexp.Regexp, code string) (string, int, return code, importCount, modErr } -func StartFromFile(file string) (*Runtime, error) { - return StartFromCode(u.ReadFileN(file), file) +func New() *Runtime { + return &Runtime{ + vm: goja.New(), + required: map[string]bool{}, + } } -func StartFromCode(code, refFile string) (*Runtime, error) { - if refFile == "" { - refFile = "main.js" +func (rt *Runtime) StartFromFile(file string) (any, error) { + return rt.StartFromCode(u.ReadFileN(file), file) +} + +func (rt *Runtime) StartFromCode(code, refFile string) (any, error) { + if refFile != "" { + rt.file = refFile + } + if rt.file == "" { + rt.file = "main.js" } - if absFile, err := filepath.Abs(refFile); err == nil { - refFile = absFile + if absFile, err := filepath.Abs(rt.file); err == nil { + rt.file = absFile } InitFrom(filepath.Dir(refFile)) - rt := &Runtime{ - vm: goja.New(), - required: map[string]bool{}, - file: refFile, - srcCode: code, - code: code, + if rt.srcCode == "" { + rt.srcCode = code } + rt.code = code // 按需加载引用 var importCount int @@ -166,9 +188,16 @@ func StartFromCode(code, refFile string) (*Runtime, error) { if !strings.HasSuffix(refPath, ".js") && !u.FileExists(refPath) { refPath += ".js" } - modCode, err := u.ReadFile(refPath) - if err != nil { - return nil, err + modCode := "" + if rt.moduleLoader != nil { + modCode = rt.moduleLoader(refPath) + } + if modCode == "" { + var err error + modCode, err = u.ReadFile(refPath) + if err != nil { + return nil, err + } } modCode, _, _ = rt.makeImport(importLibMatcher, modCode) modCode, _, _ = rt.makeImport(requireLibMatcher, modCode) @@ -179,14 +208,14 @@ func StartFromCode(code, refFile string) (*Runtime, error) { if !checkMainMatcher.MatchString(rt.code) { rt.code = "function main(...args){" + rt.code + "}" } - if _, err := rt.vm.RunScript("main", rt.code); err != nil { + if r, err := rt.vm.RunScript("main", rt.code); err != nil { return nil, err + } else { + return r, nil } - - return rt, nil } -func (rt *Runtime) Run(args ...any) (any, error) { +func (rt *Runtime) RunMain(args ...any) (any, error) { // 解析参数 for i, arg := range args { if str, ok := arg.(string); ok { @@ -211,6 +240,18 @@ func (rt *Runtime) Run(args ...any) (any, error) { return result, err } +func (rt *Runtime) RunCode(code string) (any, error) { + jsResult, err := rt.vm.RunScript(rt.file, code) + + var result any + if err == nil { + if jsResult != nil && !jsResult.Equals(goja.Undefined()) { + result = jsResult.Export() + } + } + return result, err +} + type Exports struct { LLMList []string } @@ -241,9 +282,11 @@ func ExportForDev() (string, error) { _ = u.WriteFile(filepath.Join("lib", "console.ts"), consoleTS) _ = u.WriteFile(filepath.Join("lib", "file.ts"), fileTS) _ = u.WriteFile(filepath.Join("lib", "util.ts"), utilTS) + _ = u.WriteFile(filepath.Join("lib", "http.ts"), httpTS) return `import {` + strings.Join(exports.LLMList, ", ") + `} from './lib/ai' import console from './lib/console' import util from './lib/util' +import http from './lib/http' import file from './lib/file'`, nil } diff --git a/js/ai.go b/js/ai.go index 4f60a32..d6c1722 100644 --- a/js/ai.go +++ b/js/ai.go @@ -27,49 +27,49 @@ func RequireAI(lm llm.LLM) map[string]any { "ask": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { conf, cb := getAskArgs(args.This, vm, args.Arguments) result, usage, err := lm.Ask(makeChatMessages(args.Arguments), conf, cb) - return vm.ToValue(ChatResult{TokenUsage: usage, Result: result, Error: getErrorStr(err)}) + return vm.ToValue(map[string]any{"tokenUsage": toMap(usage), "result": result, "error": getErrorStr(err)}) }, "fastAsk": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { _, cb := getAskArgs(args.This, vm, args.Arguments) result, usage, err := lm.FastAsk(makeChatMessages(args.Arguments), cb) - return vm.ToValue(ChatResult{TokenUsage: usage, Result: result, Error: getErrorStr(err)}) + return vm.ToValue(map[string]any{"tokenUsage": toMap(usage), "result": result, "error": getErrorStr(err)}) }, "longAsk": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { _, cb := getAskArgs(args.This, vm, args.Arguments) result, usage, err := lm.LongAsk(makeChatMessages(args.Arguments), cb) - return vm.ToValue(ChatResult{TokenUsage: usage, Result: result, Error: getErrorStr(err)}) + return vm.ToValue(map[string]any{"tokenUsage": toMap(usage), "result": result, "error": getErrorStr(err)}) }, "batterAsk": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { _, cb := getAskArgs(args.This, vm, args.Arguments) result, usage, err := lm.BatterAsk(makeChatMessages(args.Arguments), cb) - return vm.ToValue(ChatResult{TokenUsage: usage, Result: result, Error: getErrorStr(err)}) + return vm.ToValue(map[string]any{"tokenUsage": toMap(usage), "result": result, "error": getErrorStr(err)}) }, "bestAsk": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { _, cb := getAskArgs(args.This, vm, args.Arguments) result, usage, err := lm.BestAsk(makeChatMessages(args.Arguments), cb) - return vm.ToValue(ChatResult{TokenUsage: usage, Result: result, Error: getErrorStr(err)}) + return vm.ToValue(map[string]any{"tokenUsage": toMap(usage), "result": result, "error": getErrorStr(err)}) }, "multiAsk": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { _, cb := getAskArgs(args.This, vm, args.Arguments) result, usage, err := lm.MultiAsk(makeChatMessages(args.Arguments), cb) - return vm.ToValue(ChatResult{TokenUsage: usage, Result: result, Error: getErrorStr(err)}) + return vm.ToValue(map[string]any{"tokenUsage": toMap(usage), "result": result, "error": getErrorStr(err)}) }, "bestMultiAsk": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { _, cb := getAskArgs(args.This, vm, args.Arguments) result, usage, err := lm.BestMultiAsk(makeChatMessages(args.Arguments), cb) - return vm.ToValue(ChatResult{TokenUsage: usage, Result: result, Error: getErrorStr(err)}) + return vm.ToValue(map[string]any{"tokenUsage": toMap(usage), "result": result, "error": getErrorStr(err)}) }, "codeInterpreterAsk": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { _, cb := getAskArgs(args.This, vm, args.Arguments) result, usage, err := lm.CodeInterpreterAsk(makeChatMessages(args.Arguments), cb) - return vm.ToValue(ChatResult{TokenUsage: usage, Result: result, Error: getErrorStr(err)}) + return vm.ToValue(map[string]any{"tokenUsage": toMap(usage), "result": result, "error": getErrorStr(err)}) }, "webSearchAsk": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { _, cb := getAskArgs(args.This, vm, args.Arguments) result, usage, err := lm.WebSearchAsk(makeChatMessages(args.Arguments), cb) - return vm.ToValue(ChatResult{TokenUsage: usage, Result: result, Error: getErrorStr(err)}) + return vm.ToValue(map[string]any{"tokenUsage": toMap(usage), "result": result, "error": getErrorStr(err)}) }, "makeImage": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { @@ -128,12 +128,12 @@ func makeAIGCResult(vm *goja.Runtime, results []string, previews []string, err e } else { previews = make([]string, 0) } - return vm.ToValue(AIGCResult{ - Result: result, - Preview: preview, - Results: results, - Previews: previews, - Error: getErrorStr(err), + return vm.ToValue(map[string]any{ + "result": result, + "preview": preview, + "results": results, + "previews": previews, + "error": getErrorStr(err), }) } @@ -193,17 +193,19 @@ func makeChatMessages(args []goja.Value) []llm.ChatMessage { hasSub := false hasMulti := false for i := 0; i < vv.Len(); i++ { - if vv.Index(i).Kind() == reflect.Slice || vv.Index(i).Kind() == reflect.Map || vv.Index(i).Kind() == reflect.Struct { + vv2 := u.FinalValue(vv.Index(i)) + if vv2.Kind() == reflect.Slice || vv2.Kind() == reflect.Map || vv2.Kind() == reflect.Struct { hasSub = true break } - if vv.Index(i).Kind() == reflect.String { - str := vv.Index(i).String() + if vv2.Kind() == reflect.String { + str := vv2.String() if strings.HasPrefix(str, "data:") || strings.HasPrefix(str, "https://") || strings.HasPrefix(str, "http://") { hasMulti = true } } } + if hasSub || !hasMulti { // 有子对象或纯文本数组 var defaultRole string @@ -214,7 +216,7 @@ func makeChatMessages(args []goja.Value) []llm.ChatMessage { } else { defaultRole = llm.RoleAssistant } - vv2 := vv.Index(i) + vv2 := u.FinalValue(vv.Index(i)) switch vv2.Kind() { case reflect.Slice: out = append(out, makeChatMessageFromSlice(vv2, defaultRole)) diff --git a/js/common.go b/js/common.go new file mode 100644 index 0000000..23c036b --- /dev/null +++ b/js/common.go @@ -0,0 +1,84 @@ +package js + +import ( + "errors" + "github.com/dop251/goja" + "github.com/ssgo/u" +) + +func toMap(data any) map[string]any { + return u.UnJsonMap(u.FixedJson(data)) +} + +type Args struct { + This goja.Value + Arguments []goja.Value + VM *goja.Runtime +} + +func MakeArgs(args *goja.FunctionCall, vm *goja.Runtime) *Args { + return &Args{ + This: args.This, + Arguments: args.Arguments, + VM: vm, + } +} + +func (args *Args) Check(num int) goja.Value { + if len(args.Arguments) < num { + return args.VM.NewGoError(errors.New("arguments need " + u.String(num) + ", but given " + u.String(len(args.Arguments)))) + } + return nil +} + +func (args *Args) Int(index int) int { + if len(args.Arguments) > index { + return u.Int(args.Arguments[index].Export()) + } + return 0 +} + +func (args *Args) Any(index int) any { + if len(args.Arguments) > index { + return args.Arguments[index].Export() + } + return nil +} + +func (args *Args) Str(index int) string { + if len(args.Arguments) > index { + return u.String(args.Arguments[index].Export()) + } + return "" +} + +func (args *Args) Map(index int) map[string]any { + out := map[string]any{} + if len(args.Arguments) > index { + u.Convert(args.Arguments[index].Export(), &out) + } + return out +} + +func (args *Args) StrArr(startIndex int) []string { + if len(args.Arguments) > startIndex { + a := make([]string, len(args.Arguments)-startIndex) + for index := startIndex; index < len(args.Arguments); index++ { + a[index-startIndex] = u.String(args.Arguments[index].Export()) + } + return a + } + return make([]string, 0) +} + +func (args *Args) Map2StrArr(index int) []string { + headerMap := args.Map(index) + headers := make([]string, len(headerMap)*2) + i := 0 + for k, v := range headerMap { + headers[i] = k + headers[i+1] = u.String(v) + i += 2 + } + return headers +} diff --git a/js/file.go b/js/file.go index 0fc0e2e..f7e04c8 100644 --- a/js/file.go +++ b/js/file.go @@ -2,6 +2,7 @@ package js import ( "errors" + "fmt" "github.com/dop251/goja" "github.com/ssgo/u" ) @@ -10,17 +11,18 @@ func RequireFile() map[string]any { return map[string]any{ "read": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { if len(args.Arguments) < 1 { - return vm.NewGoError(errors.New("arguments need 1 given " + u.String(len(args.Arguments)))) + return vm.NewGoError(errors.New("arguments need 1, but given " + u.String(len(args.Arguments)))) } if r, err := u.ReadFile(u.String(args.Arguments[0].Export())); err == nil { return vm.ToValue(r) } else { + fmt.Println(err, "-----") return vm.NewGoError(err) } }, "write": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { if len(args.Arguments) < 2 { - return vm.NewGoError(errors.New("arguments need 2 given " + u.String(len(args.Arguments)))) + return vm.NewGoError(errors.New("arguments need 2, but given " + u.String(len(args.Arguments)))) } if err := u.WriteFileBytes(u.String(args.Arguments[0].Export()), u.Bytes(args.Arguments[0].Export())); err == nil { return nil @@ -30,7 +32,7 @@ func RequireFile() map[string]any { }, "dir": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { if len(args.Arguments) < 1 { - return vm.NewGoError(errors.New("arguments need 1 given " + u.String(len(args.Arguments)))) + return vm.NewGoError(errors.New("arguments need 1, but given " + u.String(len(args.Arguments)))) } if r, err := u.ReadDir(u.String(args.Arguments[0].Export())); err == nil { return vm.ToValue(r) @@ -40,7 +42,7 @@ func RequireFile() map[string]any { }, "stat": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { if len(args.Arguments) < 1 { - return vm.NewGoError(errors.New("arguments need 1 given " + u.String(len(args.Arguments)))) + return vm.NewGoError(errors.New("arguments need 1, but given " + u.String(len(args.Arguments)))) } return vm.ToValue(u.GetFileInfo(u.String(args.Arguments[0].Export()))) }, diff --git a/js/http.go b/js/http.go new file mode 100644 index 0000000..c4f208b --- /dev/null +++ b/js/http.go @@ -0,0 +1,151 @@ +package js + +import ( + "github.com/dop251/goja" + "github.com/ssgo/httpclient" + "github.com/ssgo/u" +) + +type Http struct { + client *httpclient.ClientPool +} + +var defaultHttp = &Http{ + client: httpclient.GetClient(0), +} + +var h2cHttp = &Http{ + client: httpclient.GetClientH2C(0), +} + +func RequireHTTP() map[string]any { + return map[string]any{ + "get": defaultHttp.Get, + "get2C": h2cHttp.Get, + "head": defaultHttp.Head, + "head2C": h2cHttp.Head, + "post": defaultHttp.Post, + "post2C": h2cHttp.Post, + "put": defaultHttp.Put, + "put2C": h2cHttp.Put, + "delete": defaultHttp.Delete, + "delete2C": h2cHttp.Delete, + "do": defaultHttp.Do, + "do2C": h2cHttp.Do, + "upload": defaultHttp.Upload, + "upload2C": h2cHttp.Upload, + "download": defaultHttp.Download, + "download2C": h2cHttp.Download, + } +} + +func makeResult(r *httpclient.Result, vm *goja.Runtime) goja.Value { + if r.Error != nil { + return vm.NewGoError(r.Error) + } + headers := map[string]string{} + for k, v := range r.Response.Header { + headers[k] = v[0] + } + return vm.ToValue(map[string]any{ + "status": r.Response.Status, + "statusCode": r.Response.StatusCode, + "headers": headers, + "data": r.Map(), + "result": r.String(), + }) +} + +func (hc *Http) Get(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := MakeArgs(&argsIn, vm) + if err := args.Check(1); err != nil { + return err + } + return makeResult(hc.client.Get(args.Str(0), args.Map2StrArr(1)...), vm) +} + +func (hc *Http) Head(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := MakeArgs(&argsIn, vm) + if err := args.Check(1); err != nil { + return err + } + return makeResult(hc.client.Head(args.Str(0), args.Map2StrArr(1)...), vm) +} + +func (hc *Http) Post(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := MakeArgs(&argsIn, vm) + if err := args.Check(2); err != nil { + return err + } + return makeResult(hc.client.Post(args.Str(0), args.Any(1), args.Map2StrArr(2)...), vm) +} + +func (hc *Http) Put(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := MakeArgs(&argsIn, vm) + if err := args.Check(2); err != nil { + return err + } + return makeResult(hc.client.Put(args.Str(0), args.Any(1), args.Map2StrArr(2)...), vm) +} + +func (hc *Http) Delete(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := MakeArgs(&argsIn, vm) + if err := args.Check(2); err != nil { + return err + } + return makeResult(hc.client.Delete(args.Str(0), args.Any(1), args.Map2StrArr(2)...), vm) +} + +func (hc *Http) Do(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := MakeArgs(&argsIn, vm) + if err := args.Check(3); err != nil { + return err + } + if len(argsIn.Arguments) == 3 { + argsIn.Arguments = append(argsIn.Arguments, vm.ToValue(nil)) + } + var r *httpclient.Result + if cb, ok := goja.AssertFunction(argsIn.Arguments[3]); ok { + r = hc.client.ManualDo(args.Str(0), args.Str(1), args.Any(2), args.Map2StrArr(4)...) + buf := make([]byte, 1024) + for { + n, err := r.Response.Body.Read(buf) + if err != nil { + break + } + _, _ = cb(argsIn.This, vm.ToValue(u.String(buf[0:n]))) + } + } else { + r = hc.client.Do(args.Str(0), args.Str(1), args.Any(2), args.Map2StrArr(4)...) + } + return makeResult(r, vm) +} + +func (hc *Http) Upload(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := MakeArgs(&argsIn, vm) + if err := args.Check(3); err != nil { + return err + } + postData := map[string]string{} + postFiles := map[string]any{} + u.Convert(args.Any(1), &postData) + u.Convert(args.Any(2), &postFiles) + r, _ := hc.client.MPost(args.Str(0), postData, postFiles, args.Map2StrArr(3)...) + return makeResult(r, vm) +} + +func (hc *Http) Download(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := MakeArgs(&argsIn, vm) + if err := args.Check(3); err != nil { + return err + } + var r *httpclient.Result + if cb, ok := goja.AssertFunction(argsIn.Arguments[3]); ok { + r, _ = hc.client.Download(args.Str(0), args.Str(1), func(start, end int64, ok bool, finished, total int64) { + _, _ = cb(argsIn.This, vm.ToValue(finished), vm.ToValue(total)) + }, args.Map2StrArr(3)...) + } else { + r, _ = hc.client.Download(args.Str(0), args.Str(1), nil, args.Map2StrArr(3)...) + } + return makeResult(r, vm) +} diff --git a/js/lib/http.ts b/js/lib/http.ts new file mode 100644 index 0000000..60a5421 --- /dev/null +++ b/js/lib/http.ts @@ -0,0 +1,45 @@ +// just for develop + +export default { + get, + get2C, + head, + head2C, + post, + post2C, + put, + put2C, + delete:delete_, + delete2C, + do:do_, + do2C, + upload, + upload2C, + download, + download2C, +} + +function get(url: string, headers?:Object): Result {return null} +function get2C(url: string, headers?:Object): Result {return null} +function head(url: string, headers?:Object): Result {return null} +function head2C(url: string, headers?:Object): Result {return null} +function post(url: string, data:Object, headers?:Object): Result {return null} +function post2C(url: string, data:Object, headers?:Object): Result {return null} +function put(url: string, data:Object, headers?:Object): Result {return null} +function put2C(url: string, data:Object, headers?:Object): Result {return null} +function delete_(url: string, data:Object, headers?:Object): Result {return null} +function delete2C(url: string, data:Object, headers?:Object): Result {return null} +function do_(method: string, url: string, data:Object, callback?:(data: string) => void, headers?:Object): Result {return null} +function do2C(method: string, url: string, data:Object, callback?:(data: string) => void, headers?:Object): Result {return null} +function upload(url: string, form:Object, files:Object, headers?:Object): Result {return null} +function upload2C(url: string, form:Object, files:Object, headers?:Object): Result {return null} +function download(filename: string, url: string, callback?:(finished: number, total: number) => void, headers?:Object): Result {return null} +function download2C(filename: string, url: string, callback?:(finished: number, total: number) => void, headers?:Object): Result {return null} + +interface Result { + status: string + statusCode: number + headers: Object + data: Object + result: string +} diff --git a/js/lib/util.ts b/js/lib/util.ts index 2495e57..e8d16ee 100644 --- a/js/lib/util.ts +++ b/js/lib/util.ts @@ -23,7 +23,8 @@ export default { sha1, sha256, sha512, - tpl + tpl, + shell } function json(data:any): string {return ''} @@ -49,3 +50,4 @@ function sha1(data:any): string {return ''} function sha256(data:any): string {return ''} function sha512(data:any): string {return ''} function tpl(text:string, data:any): string {return ''} +function shell(cmd:string, ...args:string[]): string[] {return []} diff --git a/js/util.go b/js/util.go index 45ae447..1b4305b 100644 --- a/js/util.go +++ b/js/util.go @@ -15,7 +15,7 @@ func RequireUtil() map[string]any { return map[string]any{ "json": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { if len(args.Arguments) < 1 { - return vm.NewGoError(errors.New("arguments need 1 given " + u.String(len(args.Arguments)))) + return vm.NewGoError(errors.New("arguments need 1, but given " + u.String(len(args.Arguments)))) } if r, err := json.Marshal(args.Arguments[0].Export()); err == nil { return vm.ToValue(string(r)) @@ -25,7 +25,7 @@ func RequireUtil() map[string]any { }, "jsonP": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { if len(args.Arguments) < 1 { - return vm.NewGoError(errors.New("arguments need 1 given " + u.String(len(args.Arguments)))) + return vm.NewGoError(errors.New("arguments need 1, but given " + u.String(len(args.Arguments)))) } if r, err := json.Marshal(args.Arguments[0].Export()); err == nil { r1 := bytes.Buffer{} @@ -40,7 +40,7 @@ func RequireUtil() map[string]any { }, "unJson": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { if len(args.Arguments) < 1 { - return vm.NewGoError(errors.New("arguments need 1 given " + u.String(len(args.Arguments)))) + return vm.NewGoError(errors.New("arguments need 1, but given " + u.String(len(args.Arguments)))) } var r any if err := json.Unmarshal(u.Bytes(args.Arguments[0].Export()), &r); err == nil { @@ -51,7 +51,7 @@ func RequireUtil() map[string]any { }, "yaml": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { if len(args.Arguments) < 1 { - return vm.NewGoError(errors.New("arguments need 1 given " + u.String(len(args.Arguments)))) + return vm.NewGoError(errors.New("arguments need 1, but given " + u.String(len(args.Arguments)))) } if r, err := yaml.Marshal(args.Arguments[0].Export()); err == nil { return vm.ToValue(string(r)) @@ -61,7 +61,7 @@ func RequireUtil() map[string]any { }, "unYaml": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { if len(args.Arguments) < 1 { - return vm.NewGoError(errors.New("arguments need 1 given " + u.String(len(args.Arguments)))) + return vm.NewGoError(errors.New("arguments need 1, but given " + u.String(len(args.Arguments)))) } var r any if err := yaml.Unmarshal(u.Bytes(args.Arguments[0].Export()), &r); err == nil { @@ -72,37 +72,37 @@ func RequireUtil() map[string]any { }, "base64": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { if len(args.Arguments) < 1 { - return vm.NewGoError(errors.New("arguments need 1 given " + u.String(len(args.Arguments)))) + return vm.NewGoError(errors.New("arguments need 1, but given " + u.String(len(args.Arguments)))) } return vm.ToValue(u.Base64(u.Bytes(args.Arguments[0].Export()))) }, "unBase64": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { if len(args.Arguments) < 1 { - return vm.NewGoError(errors.New("arguments need 1 given " + u.String(len(args.Arguments)))) + return vm.NewGoError(errors.New("arguments need 1, but given " + u.String(len(args.Arguments)))) } return vm.ToValue(u.UnBase64String(u.String(args.Arguments[0].Export()))) }, "urlBase64": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { if len(args.Arguments) < 1 { - return vm.NewGoError(errors.New("arguments need 1 given " + u.String(len(args.Arguments)))) + return vm.NewGoError(errors.New("arguments need 1, but given " + u.String(len(args.Arguments)))) } return vm.ToValue(u.UrlBase64String(u.String(args.Arguments[0].Export()))) }, "unUrlBase64": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { if len(args.Arguments) < 1 { - return vm.NewGoError(errors.New("arguments need 1 given " + u.String(len(args.Arguments)))) + return vm.NewGoError(errors.New("arguments need 1, but given " + u.String(len(args.Arguments)))) } return vm.ToValue(u.UnUrlBase64String(u.String(args.Arguments[0].Export()))) }, "hex": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { if len(args.Arguments) < 1 { - return vm.NewGoError(errors.New("arguments need 1 given " + u.String(len(args.Arguments)))) + return vm.NewGoError(errors.New("arguments need 1, but given " + u.String(len(args.Arguments)))) } return vm.ToValue(hex.EncodeToString(u.Bytes(args.Arguments[0].Export()))) }, "unHex": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { if len(args.Arguments) < 1 { - return vm.NewGoError(errors.New("arguments need 1 given " + u.String(len(args.Arguments)))) + return vm.NewGoError(errors.New("arguments need 1, but given " + u.String(len(args.Arguments)))) } if r, err := hex.DecodeString(u.String(args.Arguments[0].Export())); err == nil { return vm.ToValue(string(r)) @@ -112,7 +112,7 @@ func RequireUtil() map[string]any { }, "aes": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { if len(args.Arguments) < 3 { - return vm.NewGoError(errors.New("arguments need 3 given " + u.String(len(args.Arguments)))) + return vm.NewGoError(errors.New("arguments need 3, but given " + u.String(len(args.Arguments)))) } if r, err := u.EncryptAesBytes(u.Bytes(args.Arguments[0].Export()), u.Bytes(args.Arguments[1].Export()), u.Bytes(args.Arguments[2].Export())); err == nil { return vm.ToValue(string(r)) @@ -122,7 +122,7 @@ func RequireUtil() map[string]any { }, "unAes": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { if len(args.Arguments) < 3 { - return vm.NewGoError(errors.New("arguments need 3 given " + u.String(len(args.Arguments)))) + return vm.NewGoError(errors.New("arguments need 3, but given " + u.String(len(args.Arguments)))) } if r, err := u.DecryptAesBytes(u.Bytes(args.Arguments[0].Export()), u.Bytes(args.Arguments[1].Export()), u.Bytes(args.Arguments[2].Export())); err == nil { return vm.ToValue(string(r)) @@ -132,7 +132,7 @@ func RequireUtil() map[string]any { }, "gzip": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { if len(args.Arguments) < 1 { - return vm.NewGoError(errors.New("arguments need 1 given " + u.String(len(args.Arguments)))) + return vm.NewGoError(errors.New("arguments need 1, but given " + u.String(len(args.Arguments)))) } if r, err := u.Gzip(u.Bytes(args.Arguments[0].Export())); err == nil { return vm.ToValue(string(r)) @@ -142,7 +142,7 @@ func RequireUtil() map[string]any { }, "gunzip": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { if len(args.Arguments) < 1 { - return vm.NewGoError(errors.New("arguments need 1 given " + u.String(len(args.Arguments)))) + return vm.NewGoError(errors.New("arguments need 1, but given " + u.String(len(args.Arguments)))) } if r, err := u.Gunzip(u.Bytes(args.Arguments[0].Export())); err == nil { return vm.ToValue(string(r)) @@ -165,42 +165,57 @@ func RequireUtil() map[string]any { }, "md5": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { if len(args.Arguments) < 1 { - return vm.NewGoError(errors.New("arguments need 1 given " + u.String(len(args.Arguments)))) + return vm.NewGoError(errors.New("arguments need 1, but given " + u.String(len(args.Arguments)))) } return vm.ToValue(u.MD5(u.Bytes(args.Arguments[0].Export()))) }, "sha1": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { if len(args.Arguments) < 1 { - return vm.NewGoError(errors.New("arguments need 1 given " + u.String(len(args.Arguments)))) + return vm.NewGoError(errors.New("arguments need 1, but given " + u.String(len(args.Arguments)))) } return vm.ToValue(u.Sha1(u.Bytes(args.Arguments[0].Export()))) }, "sha256": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { if len(args.Arguments) < 1 { - return vm.NewGoError(errors.New("arguments need 1 given " + u.String(len(args.Arguments)))) + return vm.NewGoError(errors.New("arguments need 1, but given " + u.String(len(args.Arguments)))) } return vm.ToValue(u.Sha256(u.Bytes(args.Arguments[0].Export()))) }, "sha512": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { if len(args.Arguments) < 1 { - return vm.NewGoError(errors.New("arguments need 1 given " + u.String(len(args.Arguments)))) + return vm.NewGoError(errors.New("arguments need 1, but given " + u.String(len(args.Arguments)))) } return vm.ToValue(u.Sha512(u.Bytes(args.Arguments[0].Export()))) }, "tpl": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { if len(args.Arguments) < 2 { - return vm.NewGoError(errors.New("arguments need 2 given " + u.String(len(args.Arguments)))) + return vm.NewGoError(errors.New("arguments need 2, but given " + u.String(len(args.Arguments)))) } var tpl *template.Template buf := bytes.NewBuffer(make([]byte, 0)) var err error - if tpl, err = template.New("tpl").Parse(u.String(args.Arguments[0])); err == nil { - err = tpl.Execute(buf, args.Arguments[1]) + if tpl, err = template.New("tpl").Parse(u.String(args.Arguments[0].Export())); err == nil { + err = tpl.Execute(buf, args.Arguments[1].Export()) } if err != nil { return vm.NewGoError(err) } return vm.ToValue(buf.String()) }, + "shell": func(args goja.FunctionCall, vm *goja.Runtime) goja.Value { + if len(args.Arguments) < 1 { + return vm.NewGoError(errors.New("arguments need 1 or more, but given " + u.String(len(args.Arguments)))) + } + a := make([]string, len(args.Arguments)-1) + for i := 1; i < len(args.Arguments); i++ { + a[i-1] = u.String(args.Arguments[i].Export()) + } + + if r, err := u.RunCommand(u.String(args.Arguments[0].Export()), a...); err == nil { + return vm.ToValue(r) + } else { + return vm.NewGoError(err) + } + }, } } diff --git a/tests/chat_test.go b/tests/chat_test.go index e58ce2a..ea93129 100644 --- a/tests/chat_test.go +++ b/tests/chat_test.go @@ -35,7 +35,7 @@ func TestAgent(t *testing.T) { //testJS(t) //testFile(t) - testUtil(t) + testHttp(t) } func testChat(t *testing.T, llmName string) { @@ -225,6 +225,12 @@ return r fmt.Println("result:", r1) } +func testHttp(t *testing.T) { + fmt.Println(ai.Run(` +return http.do('GET','https://apigo.cc',null,str=>{console.info(str)},{aaa:111,bbb:222}) +`, "")) +} + func testUtil(t *testing.T) { fmt.Println(hex.EncodeToString(u.Sha256([]byte{0, 98, 2}))) r, err := ai.Run(`