Skip to content

Commit

Permalink
Updated documentation, fixed Date export. See dop251#170, closes dop2…
Browse files Browse the repository at this point in the history
  • Loading branch information
dop251 committed Aug 10, 2020
1 parent e36d2cb commit 52a8eb1
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 56 deletions.
50 changes: 12 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ performance.

This project was largely inspired by [otto](https://github.com/robertkrimen/otto).

Minimum required Go version is 1.14.

Features
--------

* Full ECMAScript 5.1 support (yes, including regex and strict mode).
* Full ECMAScript 5.1 support (including regex and strict mode).
* Passes nearly all [tc39 tests](https://github.com/tc39/test262) tagged with es5id. The goal is to pass all of them. Note, the last working commit is https://github.com/tc39/test262/commit/1ba3a7c4a93fc93b3d0d7e4146f59934a896837d. The next commit made use of template strings which goja does not support.
* Capable of running Babel, Typescript compiler and pretty much anything written in ES5.
* Sourcemaps.
Expand All @@ -34,11 +36,11 @@ You can find some benchmarks [here](https://github.com/dop251/goja/issues/2).
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
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 external dependencies, it's very easy to build and it
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.
Expand All @@ -53,7 +55,7 @@ it's not possible to pass object values between runtimes.

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 of the NodeJS functionality
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)?
Expand All @@ -67,7 +69,8 @@ however because the version of tc39 tests I use is quite old, it may be not as w
functionality. Because ES6 is a superset of ES5.1 it should not break your existing code.

I will be adding features in their dependency order and as quickly as my time allows. Please do not ask
for ETA. Features that are open in the [milestone](https://github.com/dop251/goja/milestone/1) are either in progress or will be worked on next.
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.

### How do I contribute?

Expand Down Expand Up @@ -101,37 +104,7 @@ if num := v.Export().(int64); num != 4 {
Passing Values to JS
--------------------

Any Go value can be passed to JS using Runtime.ToValue() method. Primitive types (ints and uints, floats, string, bool)
are converted to the corresponding JavaScript primitives.

*func(FunctionCall) Value* is treated as a native JavaScript function.

*func(ConstructorCall) \*Object* is treated as a JavaScript constructor (see Native Constructors).

*map[string]interface{}* is converted into a host object that largely behaves like a JavaScript Object.

*[]interface{}* is converted into a host object that behaves largely like a JavaScript Array, however it's not extensible
because extending it can change the pointer so it becomes detached from the original.

**[]interface{}* is same as above, but the array becomes extensible.

A function is wrapped within a native JavaScript function. When called the arguments are automatically converted to
the appropriate Go types. If conversion is not possible, a TypeError is thrown.

A slice type is converted into a generic reflect based host object that behaves similar to an unexpandable Array.

A map type with numeric or string keys and no methods is converted into a host object where properties are map keys.

A map type with methods is converted into a host object where properties are method names,
the map values are not accessible. This is to avoid ambiguity between m\["Property"\] and m.Property.

Any other type is converted to a generic reflect based host object. Depending on the underlying type it behaves similar
to a Number, String, Boolean or Object. This includes pointers to primitive types (*string, *int, etc...).
Internally they remain pointers, so changes to the pointed values will be reflected in JS.

Note that these conversions wrap the original value which means any changes made inside JS
are reflected on the value and calling Export() returns the original value. This applies to all
reflect based types.
Any Go value can be passed to JS using Runtime.ToValue() method. See the method's [documentation](https://godoc.org/github.com/dop251/goja#Runtime.ToValue) for more details.

Exporting Values from JS
------------------------
Expand All @@ -144,7 +117,7 @@ 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:
dealing with a 3rd party library, you can use a [FieldNameMapper](https://godoc.org/github.com/dop251/goja#FieldNameMapper):

```go
vm := New()
Expand All @@ -158,7 +131,8 @@ fmt.Println(res.Export())
// Output: 42
```

There are two standard mappers: `TagFieldNameMapper` and `UncapFieldNameMapper`, or you can use your own implementation.
There are two standard mappers: [TagFieldNameMapper](https://godoc.org/github.com/dop251/goja#TagFieldNameMapper) and
[UncapFieldNameMapper](https://godoc.org/github.com/dop251/goja#UncapFieldNameMapper), or you can use your own implementation.

Native Constructors
-------------------
Expand Down
2 changes: 1 addition & 1 deletion date.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func (d *dateObject) toPrimitive() Value {

func (d *dateObject) export() interface{} {
if d.isSet() {
return d.time
return d.time()
}
return nil
}
Expand Down
19 changes: 19 additions & 0 deletions date_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -377,3 +377,22 @@ func TestDateMaxValues(t *testing.T) {
`
testScript1(TESTLIB+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)
}
}
30 changes: 30 additions & 0 deletions object_goreflect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1088,6 +1088,36 @@ func TestGoReflectSymbols(t *testing.T) {
}
}

func TestGoReflectSymbolEqualityQuirk(t *testing.T) {
type Field struct{
}
type S struct {
Field *Field
}
var s = S{
Field: &Field{},
}
vm := New()
vm.Set("s", &s)
res, err := vm.RunString(`
var sym = Symbol(66);
var field1 = s.Field;
field1[sym] = true;
var field2 = s.Field;
// Because a wrapper is created every time the property is accessed
// field1 and field2 will be different instances of the wrapper.
// Symbol properties only exist in the wrapper, they cannot be placed into the original Go value,
// hence the following:
field1 === field2 && field1[sym] === true && field2[sym] === undefined;
`)
if err != nil {
t.Fatal(err)
}
if res != valueTrue {
t.Fatal(res)
}
}

func TestGoObj__Proto__(t *testing.T) {
type S struct {
Field int
Expand Down
134 changes: 117 additions & 17 deletions runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -1176,23 +1176,123 @@ func (r *Runtime) ClearInterrupt() {
}

/*
ToValue converts a Go value into JavaScript value.
Primitive types (ints and uints, floats, string, bool) are converted to the corresponding JavaScript primitives.
func(FunctionCall) Value is treated as a native JavaScript function.
map[string]interface{} is converted into a host object that largely behaves like a JavaScript Object.
[]interface{} is converted into a host object that behaves largely like a JavaScript Array, however it's not extensible
because extending it can change the pointer so it becomes detached from the original.
*[]interface{} same as above, but the array becomes extensible.
A function is wrapped within a native JavaScript function. When called the arguments are automatically converted to
the appropriate Go types. If conversion is not possible, a TypeError is thrown.
A slice type is converted into a generic reflect based host object that behaves similar to an unexpandable Array.
ToValue converts a Go value into a JavaScript value of a most appropriate type. Structural types (such as structs, maps
and slices) are wrapped so that changes are reflected on the original value which can be retrieved using Value.Export().
Notes on individual types:
Primitive types.
-----
Primitive types (numbers, string, bool) are converted to the corresponding JavaScript primitives.
Strings.
-----
Because of the difference in internal string representation between ECMAScript (which uses UTF-16) and Go (which uses
UTF-8) conversion from JS to Go may be lossy. In particular, code points that can be part of UTF-16 surrogate pairs
(0xD800-0xDFFF) cannot be represented in UTF-8 unless they form a valid surrogate pair and are replaced with
utf8.RuneError.
Nil.
-----
Nil is converted to `null`.
Functions.
-----
`func(FunctionCall) Value` is treated as a native JavaScript function. This increases performance because there are no
automatic argument and return value type conversions (which involves reflect).
Any other Go function is wrapped so that the arguments are automatically converted into the required Go types and the
return value is converted to a JavaScript value (using this method). If conversion is not possible, a TypeError is
thrown.
Functions with multiple return values return an Array. If the last return value is an `error` it is not returned but
converted into a JS exception. If the error is *Exception, it is thrown as is, otherwise it's wrapped in a GoEerror.
Note that if there are exactly two return values and the last is an `error`, the function returns the first value as is,
not an Array.
Structs.
---
Structs are converted to Object-like values. Fields and methods are available as properties, their values are
results of this method (ToValue()) applied to the corresponding Go value.
Field properties are writable (if the struct is addressable) and non-configurable.
Method properties are non-writable and non-configurable.
Attempt to define a new property or delete an existing property will fail (throw in strict mode) unless it's a Symbol
property. Symbol properties only exist in the wrapper and do not affect the underlying Go value.
Note that because a wrapper is created every time a property is accessed it may lead to unexpected results such as this:
```
type Field struct{
}
type S struct {
Field *Field
}
var s = S{
Field: &Field{},
}
vm := New()
vm.Set("s", &s)
res, err := vm.RunString(`
var sym = Symbol(66);
var field1 = s.Field;
field1[sym] = true;
var field2 = s.Field;
field1 === field2; // true, because the equality operation compares the wrapped values, not the wrappers
field1[sym] === true; // true
field2[sym] === undefined; // also true
`)
```
The same applies to values from maps and slices as well.
time.Time.
---
time.Time does not get special treatment and therefore is converted just like any other `struct` providing access to
all its methods. This is done deliberately instead of converting it to a `Date` because these two types are not fully
compatible: `time.Time` includes zone, whereas JS `Date` doesn't. Doing the conversion implicitly therefore would
result in a loss of information.
If you need to convert it to a `Date`, it can be done either in JS:
```
var d = new Date(goval.UnixNano()/1e6);
```
... or in Go:
```
now := time.Now()
vm := New()
val, err := vm.New(vm.Get("Date").ToObject(vm), vm.ToValue(now.UnixNano()/1e6))
if err != nil {
...
}
vm.Set("d", val)
```
Note that Value.Export() for a `Date` value returns time.Time in local timezone.
Maps.
---
Maps with string or integer key type are converted into host objects that largely behave like a JavaScript Object.
Maps with methods.
---
If a map type has at least one method defined, the properties of the resulting Object represent methods, not map keys.
This is because in JavaScript there is no distinction between 'object.key` and `object[key]`, unlike Go.
If access to the map values is required, it can be achieved by defining another method or, if it's not possible, by
defining an external getter function.
Slices.
---
Slices are converted into host objects that behave largely like JavaScript Array. It has the appropriate
prototype and all the usual methods should work. There are, however, some caveats:
- If the slice is not addressable, the array cannot be extended or shrunk. Any attempt to do so (by setting an index
beyond the current length or by modifying the length) will result in a TypeError.
- Converted Arrays may not contain holes (because Go slices cannot). This means that hasOwnProperty(n) will always
return `true` if n < length. Attempt to delete an item with an index < length will fail. Nil slice elements will be
converted to `null`. Accessing an element beyond `length` will return `undefined`.
Any other type is converted to a generic reflect based host object. Depending on the underlying type it behaves similar
to a Number, String, Boolean or Object.
Expand Down
25 changes: 25 additions & 0 deletions runtime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1487,6 +1487,31 @@ func TestToValueNilValue(t *testing.T) {
}
}

func TestDateConversion(t *testing.T) {
now := time.Now()
vm := New()
val, err := vm.New(vm.Get("Date").ToObject(vm), vm.ToValue(now.UnixNano()/1e6))
if err != nil {
t.Fatal(err)
}
vm.Set("d", val)
res, err := vm.RunString(`+d`)
if err != nil {
t.Fatal(err)
}
if exp := res.Export(); exp != now.UnixNano()/1e6 {
t.Fatalf("Value does not match: %v", exp)
}
vm.Set("goval", now)
res, err = vm.RunString(`+(new Date(goval.UnixNano()/1e6))`)
if err != nil {
t.Fatal(err)
}
if exp := res.Export(); exp != now.UnixNano()/1e6 {
t.Fatalf("Value does not match: %v", exp)
}
}

func TestNativeCtorNewTarget(t *testing.T) {
const SCRIPT = `
function NewTarget() {
Expand Down

0 comments on commit 52a8eb1

Please sign in to comment.