This commit is contained in:
Star 2024-09-20 16:50:35 +08:00
parent b7ae15cad0
commit e690ae764b
146 changed files with 80103 additions and 88 deletions

View File

@ -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
}

183
ai/watcher/watcher.go Normal file
View File

@ -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
}
}

7
go.mod
View File

@ -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

15
goja/LICENSE Normal file
View File

@ -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.

334
goja/README.md Normal file
View File

@ -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.

565
goja/array.go Normal file
View File

@ -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
}

500
goja/array_sparse.go Normal file
View File

@ -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)
}

264
goja/array_sparse_test.go Normal file
View File

@ -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])
}
}
}

133
goja/array_test.go Normal file
View File

@ -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)
}
}

1068
goja/ast/README.markdown Normal file

File diff suppressed because it is too large Load Diff

876
goja/ast/node.go Normal file
View File

@ -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() }

1794
goja/builtin_array.go Normal file

File diff suppressed because it is too large Load Diff

367
goja/builtin_arrray_test.go Normal file
View File

@ -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)
}

369
goja/builtin_bigint.go Normal file
View File

@ -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
}

117
goja/builtin_bigint_test.go Normal file
View File

@ -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)
}

75
goja/builtin_boolean.go Normal file
View File

@ -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
}

1058
goja/builtin_date.go Normal file

File diff suppressed because it is too large Load Diff

314
goja/builtin_error.go Normal file
View File

@ -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
}

416
goja/builtin_function.go Normal file
View File

@ -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
}

View File

@ -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)
}

576
goja/builtin_global.go Normal file
View File

@ -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
}

View File

@ -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)
}

542
goja/builtin_json.go Normal file
View File

@ -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
}

140
goja/builtin_json_test.go Normal file
View File

@ -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)
}
}

342
goja/builtin_map.go Normal file
View File

@ -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
}

244
goja/builtin_map_test.go Normal file
View File

@ -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)
}
}
}

358
goja/builtin_math.go Normal file
View File

@ -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
}

303
goja/builtin_number.go Normal file
View File

@ -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
}

711
goja/builtin_object.go Normal file
View File

@ -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
}

646
goja/builtin_promise.go Normal file
View File

@ -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
}

396
goja/builtin_proxy.go Normal file
View File

@ -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
}

1275
goja/builtin_proxy_test.go Normal file

File diff suppressed because it is too large Load Diff

140
goja/builtin_reflect.go Normal file
View File

@ -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
}

1348
goja/builtin_regexp.go Normal file

File diff suppressed because it is too large Load Diff

346
goja/builtin_set.go Normal file
View File

@ -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
}

180
goja/builtin_set_test.go Normal file
View File

@ -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)
}

1112
goja/builtin_string.go Normal file

File diff suppressed because it is too large Load Diff

288
goja/builtin_string_test.go Normal file
View File

@ -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)
}

177
goja/builtin_symbol.go Normal file
View File

@ -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
}

1973
goja/builtin_typedarrays.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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)
}

176
goja/builtin_weakmap.go Normal file
View File

@ -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
}

View File

@ -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)
}

135
goja/builtin_weakset.go Normal file
View File

@ -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
}

View File

@ -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)
}

1481
goja/compiler.go Normal file

File diff suppressed because it is too large Load Diff

3613
goja/compiler_expr.go Normal file

File diff suppressed because it is too large Load Diff

1127
goja/compiler_stmt.go Normal file

File diff suppressed because it is too large Load Diff

5900
goja/compiler_test.go Normal file

File diff suppressed because it is too large Load Diff

170
goja/date.go Normal file
View File

@ -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)
}

869
goja/date_parser.go Normal file
View File

@ -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<<stdArgShift - 1 // mask out argument
)
var errBad = errors.New("bad value for field") // placeholder not passed to user
func parseDate(layout, value string, defaultLocation *time.Location) (time.Time, error) {
alayout, avalue := layout, value
rangeErrString := "" // set if a value is out of range
amSet := false // do we need to subtract 12 from the hour for midnight?
pmSet := false // do we need to add 12 to the hour?
// Time being constructed.
var (
year int
month int = 1 // January
day int = 1
hour int
min int
sec int
nsec int
z *time.Location
zoneOffset int = -1
zoneName string
)
// Each iteration processes one std value.
for {
var err error
prefix, std, suffix := nextStdChunk(layout)
stdstr := layout[len(prefix) : len(layout)-len(suffix)]
value, err = skip(value, prefix)
if err != nil {
return time.Time{}, &time.ParseError{Layout: alayout, Value: avalue, LayoutElem: prefix, ValueElem: value}
}
if std == 0 {
if len(value) != 0 {
return time.Time{}, &time.ParseError{Layout: alayout, Value: avalue, ValueElem: value, Message: ": extra text: " + value}
}
break
}
layout = suffix
var p string
switch std & stdMask {
case stdYear:
if len(value) < 2 {
err = errBad
break
}
p, value = value[0:2], value[2:]
year, err = atoi(p)
if year >= 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'
}

31
goja/date_parser_test.go Normal file
View File

@ -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))
}

309
goja/date_test.go Normal file
View File

@ -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)
}
}

300
goja/destruct.go Normal file
View File

@ -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)
}

3
goja/extract_failed_tests.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
sed -En 's/^.*FAIL: TestTC39\/tc39\/(test\/.*.js).*$/"\1": true,/p'

110
goja/file/README.markdown Normal file
View File

@ -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

234
goja/file/file.go Normal file
View File

@ -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
}

75
goja/file/file_test.go Normal file
View File

@ -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)
}
}
}

21
goja/ftoa/LICENSE_LUCENE Normal file
View File

@ -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.

150
goja/ftoa/common.go Normal file
View File

@ -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
}

699
goja/ftoa/ftoa.go Normal file
View File

@ -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
}
}

153
goja/ftoa/ftobasestr.go Normal file
View File

@ -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()
}
}

View File

@ -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)
}
}

147
goja/ftoa/ftostr.go Normal file
View File

@ -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 <precision> digits after the decimal point; exponential if number is large
ModeFixed
// Always exponential format; <precision> significant digits
ModeExponential
// Either fixed or exponential format; <precision> 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
}

92
goja/ftoa/ftostr_test.go Normal file
View File

@ -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)
}
}

View File

@ -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.

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}

1116
goja/func.go Normal file

File diff suppressed because it is too large Load Diff

309
goja/func_test.go Normal file
View File

@ -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)
}
})
}

15
goja/go.mod Normal file
View File

@ -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

127
goja/goja/main.go Normal file
View File

@ -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 = "<stdin>"
}
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)
}
}

98
goja/ipow.go Normal file
View File

@ -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
}
}

169
goja/map.go Normal file
View File

@ -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
}

201
goja/map_test.go Normal file
View File

@ -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)
}
}

1824
goja/object.go Normal file

File diff suppressed because it is too large Load Diff

139
goja/object_args.go Normal file
View File

@ -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
}

794
goja/object_dynamic.go Normal file
View File

@ -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)
}

420
goja/object_dynamic_test.go Normal file
View File

@ -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)
}
}

View File

@ -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
}
}
}

View File

@ -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)
}
}

158
goja/object_gomap.go Normal file
View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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)
})
}

328
goja/object_gomap_test.go Normal file
View File

@ -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)
}
}

674
goja/object_goreflect.go Normal file
View File

@ -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{}
}

File diff suppressed because it is too large Load Diff

343
goja/object_goslice.go Normal file
View File

@ -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]
}

View File

@ -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)
}

View File

@ -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)
}
}

298
goja/object_goslice_test.go Normal file
View File

@ -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)
}

469
goja/object_template.go Normal file
View File

@ -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
}

668
goja/object_test.go Normal file
View File

@ -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: <nil>
}
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)
}
}
}

184
goja/parser/README.markdown Normal file
View File

@ -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

176
goja/parser/error.go Normal file
View File

@ -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
}

1660
goja/parser/expression.go Normal file

File diff suppressed because it is too large Load Diff

1205
goja/parser/lexer.go Normal file

File diff suppressed because it is too large Load Diff

423
goja/parser/lexer_test.go Normal file
View File

@ -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,
)
})
}

Some files were not shown because too many files have changed in this diff Show More